高性能的圖片壓縮 —— libjpeg-turbo 的編譯與集成

前言

Android 提供的 JPEG 壓縮, 是由外部鏈接庫中的 libjpeg 實現的, 但 Google 考慮到 Android 設備性能的瓶頸, 在 Skia 調用中的三方鏈接庫 libjpeg 時, 多處進行了閹割處理, 這樣帶來的好處就是壓縮的速度更快了, 但細節丟失嚴重, 壓縮後甚至有偏綠的情況, 下面的代碼便是 Android 執行 JPEG 壓縮的關鍵

/**
 * SkImageDecoder_libjpeg.cpp
 */
class SkJPEGImageEncoder : public SkImageEncoder {
protected:
    virtual bool onEncode(SkWStream* stream, const SkBitmap& bm, int quality) {
        ......
        // 1. 初始化 libjpeg
        jpeg_create_compress(&cinfo);
        // 設置一些參數
        cinfo.dest = &sk_wstream;
        cinfo.image_width = bm.width();
        cinfo.image_height = bm.height();
        cinfo.input_components = 3;
        // FIXME: Can we take advantage of other in_color_spaces in libjpeg-turbo?
        cinfo.in_color_space = JCS_RGB;
        // The gamma value is ignored by libjpeg-turbo.
        cinfo.input_gamma = 1;
        jpeg_set_defaults(&cinfo);
        // 這個標誌用於控制是否使用優化的哈夫曼表
        cinfo.optimize_coding = TRUE;
        jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
        
        // 2. 開始壓縮
        jpeg_start_compress(&cinfo, TRUE);

        const int       width = bm.width();
        uint8_t*        oneRowP = oneRow.reset(width * 3);

        const SkPMColor* colors = bm.getColorTable() ? bm.getColorTable()->readColors() : nullptr;
        const void*      srcRow = bm.getPixels();
        
        while (cinfo.next_scanline < cinfo.image_height) {
            JSAMPROW row_pointer[1];    /* pointer to JSAMPLE row[s] */
            writer(oneRowP, srcRow, width, colors);
            row_pointer[0] = oneRowP;
            (void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
            srcRow = (const void*)((const char*)srcRow + bm.rowBytes());
        }
        
        // 3. 結束壓縮
        jpeg_finish_compress(&cinfo);
        
        // 4. 釋放內存
        jpeg_destroy_compress(&cinfo);

        return true;
    }
};

從上面的代碼中, 我們定位到 cinfo.optimize_coding 這個參數

  • Android7.0 之後, 這個參數爲 true
    • 在圖片壓縮的時候, 會根據圖片去計算其對應的哈夫曼表, 圖片質量更高, 但是圖片佔用的磁盤空間也相應更高
  • Android7.0 之前, 這個參數爲 false
    • 使用默認的哈夫曼表, 不會去根據圖片進行特定的計算, 經 Google 測試, 圖片質量比使用哈夫曼低兩倍左右

初次之外早期的 Android 版本, 同樣考慮到性能問題, skia 引擎寫了一個函數替代了原來 libjpeg 的轉換函數, 好處是提高了編碼速度, 壞處就是犧牲了每一個像素的精度

爲了實現更快速更高質量的 JPEG 有損壓縮, 因此筆者選擇編譯 libjpeg-turbo, 來處理項目中的圖片壓縮, 據官方介紹, 得益於它高度優化的哈夫曼算法, 它比 libjpeg 要快上 2-6 倍, 接下來我們來一步一步的將它集成到項目中

一. 準備工作

一) 操作系統

Ubuntu-18.04.1

二) 依賴安裝

1. NDK

android-ndk-r16b-linux-x86_64.zip

2. CMake

CMake 3.12.1 的 Linux 版本

3. make

sudo apt-get install make

4. libjpeg-turbo

從 Github 上下載最新的源碼即可

https://github.com/libjpeg-turbo/libjpeg-turbo

註釋版本號
  • 打開 libjpeg-turbo/sharedLibs/CMakeList.txt, 將設置版本號的位置註釋, 否則在使用時, 可能會出現運行時缺少 so 庫的問題

二. 編譯

一) 腳本編寫

Android 端腳本編寫指南在 libjpeg-turbo 庫中的 BUILDING.md 中有說明

Building libjpeg-turbo for Android
----------------------------------

Building libjpeg-turbo for Android platforms requires v13b or later of the
[Android NDK](https://developer.android.com/tools/sdk/ndk).


### ARMv7 (32-bit)

The following is a general recipe script that can be modified for your specific
needs.

    # Set these variables to suit your needs
    NDK_PATH={full path to the NDK directory-- for example,
      /opt/android/android-ndk-r16b}
    TOOLCHAIN={"gcc" or "clang"-- "gcc" must be used with NDK r16b and earlier,
      and "clang" must be used with NDK r17c and later}
    ANDROID_VERSION={the minimum version of Android to support-- for example,
      "16", "19", etc.}

    cd {build_directory}
    cmake -G"Unix Makefiles" \
      -DANDROID_ABI=armeabi-v7a \
      -DANDROID_ARM_MODE=arm \
      -DANDROID_PLATFORM=android-${ANDROID_VERSION} \
      -DANDROID_TOOLCHAIN=${TOOLCHAIN} \
      -DCMAKE_ASM_FLAGS="--target=arm-linux-androideabi${ANDROID_VERSION}" \
      -DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
      [additional CMake flags] {source_directory}
    make
......

我們按照它的要求, 進行 shell 腳本的編寫即可, 編寫後的shell 腳本如下

#!/bin/sh

# lib-name
MY_LIBS_NAME=libjpeg-turbo
# 源碼文件目錄
MY_SOURCE_DIR=/home/sharry/Desktop/libjpeg-turbo-master
# 編譯的過程中產生的中間件的存放目錄,爲了區分編譯目錄,源碼目錄,install目錄
MY_BUILD_DIR=binary

##  CMake 環境變量
export PATH=/home/sharry/Desktop/cmake-3.12.1-Linux-x86_64/bin:$PATH

NDK_PATH=/home/sharry/Desktop/android-ndk-r16b
BUILD_PLATFORM=linux-x86_64
TOOLCHAIN_VERSION=4.9
ANDROID_VERSION=19

ANDROID_ARMV5_CFLAGS="-march=armv5te"
ANDROID_ARMV7_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon"  # -mfpu=vfpv3-d16  -fexceptions -frtti
ANDROID_ARMV8_CFLAGS="-march=armv8-a"   # -mfloat-abi=softfp -mfpu=neon -fexceptions -frtti
ANDROID_X86_CFLAGS="-march=i386 -mtune=intel -mssse3 -mfpmath=sse -m32"
ANDROID_X86_64_CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"

# params($1:arch,$2:arch_abi,$3:host,$4:compiler,$5:cflags,$6:processor)
build_bin() {

    echo "-------------------star build $2-------------------------"

    ARCH=$1                # arm arm64 x86 x86_64
    ANDROID_ARCH_ABI=$2    # armeabi armeabi-v7a x86 mips
    # 最終編譯的安裝目錄
    PREFIX=$(pwd)/dist/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}/
    HOST=$3
    COMPILER=$4
    PROCESSOR=$6
    SYSROOT=${NDK_PATH}/platforms/android-${ANDROID_VERSION}/arch-${ARCH}
    CFALGS="$5"
    TOOLCHAIN=${NDK_PATH}/toolchains/${HOST}-${TOOLCHAIN_VERSION}/prebuilt/${BUILD_PLATFORM}
    
    # build 中間件
    BUILD_DIR=./${MY_BUILD_DIR}/${ANDROID_ARCH_ABI}

    export CFLAGS="$5 -Os -D__ANDROID_API__=${ANDROID_VERSION} --sysroot=${SYSROOT} \
                   -isystem ${NDK_PATH}/sysroot/usr/include \
                   -isystem ${NDK_PATH}/sysroot/usr/include/${HOST} "
    export LDFLAGS=-pie

    echo "path==>$PATH"
    echo "build_dir==>$BUILD_DIR"
    echo "ARCH==>$ARCH"
    echo "ANDROID_ARCH_ABI==>$ANDROID_ARCH_ABI"
    echo "HOST==>$HOST"
    echo "CFALGS==>$CFALGS"
    echo "COMPILER==>$COMPILER-gcc"
    echo "PROCESSOR==>$PROCESSOR"

    mkdir -p ${BUILD_DIR}   #創建當前arch_abi的編譯目錄,比如:binary/armeabi-v7a
    cd ${BUILD_DIR}         #此處 進了當前arch_abi的2級編譯目錄

# 運行時創建臨時編譯鏈文件toolchain.cmake
cat >toolchain.cmake << EOF 
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR $6)
set(CMAKE_C_COMPILER ${TOOLCHAIN}/bin/${COMPILER}-gcc)
set(CMAKE_FIND_ROOT_PATH ${TOOLCHAIN}/${COMPILER})
EOF

    cmake -G"Unix Makefiles" \
          -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \
          -DCMAKE_POSITION_INDEPENDENT_CODE=1 \
          -DCMAKE_INSTALL_PREFIX=${PREFIX} \
          -DWITH_JPEG8=1 \
          ${MY_SOURCE_DIR}

    make clean
    make
    make install

    #從當前arch_abi編譯目錄跳出,對應上面的cd ${BUILD_DIR},以便function多次執行
    cd ../../

    echo "-------------------$2 build end-------------------------"
}

# build armeabi
build_bin arm armeabi arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV5_CFLAGS" arm

二) 執行編譯腳本

sh build.sh

編譯執行之後, 便會輸出頭文件 和 armeabi 架構的 so 庫

三. 集成

一) 添加

將我們上面編譯好的 so 和頭文件拷貝到我們的項目中

二) CMake 鏈接

在 CMake 中將我們的動態了添加進去

# 鏈接頭文件
include_directories(${source_dir}/jniLibs/include)

# libjpeg-turbo
add_library(libjpeg SHARED IMPORTED)
set_target_properties(
        libjpeg
        PROPERTIES
        IMPORTED_LOCATION
        ${source_dir}/jniLibs/armeabi/libjpeg.so
)

# 將打包的 so 鏈接到項目中
target_link_libraries(
        ......
        libjpeg
        ......
)

三) build.gradle

因爲我們只編譯了 armeabi 架構的 so, 因此我們需要再 gradle 中添加 filters

android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        externalNativeBuild {
            ndk {
                abiFilters "armeabi" // 只生成 armeabi 的 CPU 架構的 .so
            }
        }
    }
}

好的, 至此我們的集成就完成了, 接下來提供一些簡單的用法

四. 代碼的編寫與測試

我們編譯 libjpeg-turbo 的主要目的就是爲了進行 JPEG 的高質量壓縮, 關於 libjpeg-turbo 的使用, 這裏就不贅述了, 其官方提供好的 sample 如下

https://raw.githubusercontent.com/libjpeg-turbo/libjpeg-turbo/master/example.txt

簡單的來說, 就是將 Bitmap 的顏色通道轉爲 BGR, 然後傳給 libjpeg-turbo API 即可, 代碼還是非常簡單的

extern "C"
JNIEXPORT jint JNICALL
Java_com_sharry_libscompressor_Core_nativeCompress(JNIEnv *env, jclass type, jobject bitmap,
                                                   jint quality, jstring destPath_) {
    // 1. 獲取 bitmap 信息
    AndroidBitmapInfo info;
    AndroidBitmap_getInfo(env, bitmap, &info);
    int cols = info.width;
    int rows = info.height;
    int format = info.format;
    LOGE("->> Bitmap width is %d, height is %d", cols, rows);
    // 若不爲 ARGB8888, 則不給予壓縮
    if (format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
        return false;
    }
    // 2. 解析數據
    LOGE("->> Parse bitmap pixels");
    // 鎖定畫布
    uchar *pixels = NULL;
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
    // 創建存儲數組
    uchar *data = (uchar *) malloc(static_cast<size_t>(cols * rows * 3));
    uchar *data_header_pointer = data;// 臨時保存 data 的首地址, 用於後續釋放內存
    uchar r, g, b;
    int row = 0, col = 0, pixel;
    for (row = 0; row < rows; ++row) {
        for (col = 0; col < cols; ++col) {
            // 獲取二維數組的每一個像素信息首地址
            pixel = *((int *) pixels);
            // ...                                              // 忽略 A 通道值
            r = static_cast<uchar>((pixel & 0x00FF0000) >> 16); // 獲取 R 通道值
            g = static_cast<uchar>((pixel & 0x0000FF00) >> 8);  // 獲取 G 通道值
            b = static_cast<uchar>((pixel & 0x000000FF));       // 獲取 B 通道值
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            data += 3;
            pixels += 4;
        }
    }
    // 解鎖畫布
    AndroidBitmap_unlockPixels(env, bitmap);
    // 3. 使用 libjpeg 進行圖片質量壓縮
    LOGE("->> Lib jpeg turbo do compress");
    char *output_filename = (char *) (env)->GetStringUTFChars(destPath_, 0);
    int result = LibJpegTurboUtils::write_JPEG_file(data_header_pointer, rows, cols, output_filename,
                                               quality);
    // 4. 釋放資源
    LOGE("->> Release memory");
    free((void *) data_header_pointer);
    env->ReleaseStringUTFChars(destPath_, output_filename);
    return result;
}

效果展示

I/Core: Request{inputSourceType = String, outputSourceType = Bitmap, quality = 70, destWidth = -1, destHeight = -1}
// 採樣壓縮之後
E/Core_native: ->> Bitmap width is 1512, height is 2016
E/Core_native: ->> Parse bitmap pixels
E/Core_native: ->> Lib jpeg turbo do compress
E/Core_native: ->> Release memory
I/Core: ->> output file is: /data/user/0/com.sharry.scompressor/cache/1555157510264.jpg
// 質量壓縮之後
I/Core: ->> Output file length is 196kb

可以看到 1512 x 2016 的圖片, 在 quality 爲 70 的情況下壓縮之後, 爲 196kb, 當然他的依舊是非常清晰的

總結

到這裏我們的編譯與集成就完成了, 整體的過程還是比較簡單的, 其效果也非常的 nice, 而且不會受到 Android SDK 版本的困擾, 感興趣的同學可以按照上述的方式試試看。

參考文獻

https://blog.csdn.net/yuxiatongzhi/article/details/81743249

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章