Android 提供的 JPEG 壓縮, 是由外部連接庫中的 libjpeg 實現的, 但 Google 考慮到 Android 設備性能的瓶頸, 在 Skia 調用中的三方連接庫 libjpeg 時, 多處進行了閹割處理, 這樣帶來的好處就是壓縮的速度更快了, 但細節丟失嚴重, 壓縮後甚至有偏綠的狀況, 下面的代碼即是 Android 執行 JPEG 壓縮的關鍵linux
/**
* 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 這個參數android
除此以外早期的 Android 版本, 一樣考慮到性能問題, skia 引擎寫了一個函數替代了原來 libjpeg 的轉換函數, 好處是提升了編碼速度, 壞處就是犧牲了每個像素的精度c++
爲了實現更快速更高質量的 JPEG 有損壓縮, 所以筆者選擇編譯 libjpeg-turbo, 來處理項目中的圖片壓縮, 據官方介紹, 得益於它高度優化的哈夫曼算法, 它比 libjpeg 要快上 2-6 倍, 接下來咱們來一步一步的將它集成到項目中git
MacOS Mojave version 10.14.5github
從 Github 上下載最新的源碼便可
github.com/libjpeg-tur…算法
NDK16shell
➜ ~ cmake -version
cmake version 3.14.5
複製代碼
爲了方便使用, 咱們須要先註釋版本號數組
#set_target_properties(jpeg PROPERTIES SOVERSION ${SO_MAJOR_VERSION}
# VERSION ${SO_MAJOR_VERSION}.${SO_AGE}.${SO_MINOR_VERSION})
複製代碼
Android 端腳本編寫指南在 libjpeg-turbo 庫中的 BUILDING.md 中有說明bash
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 腳本以下架構
# 定義變量
ARCH=arm
ANDROID_ARCH_ABI=armeabi-v7a
ANDROID_VERSION=19
NDK_PATH=/Users/sharrychoo/Library/Android/ndk/android-ndk-r16b
PREFIX=`pwd`/android/${ARCH}/${CPU}
CFALGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon"
# 使用 cmake 命令生成 Makefile
cmake -G"Unix Makefiles" \
-DANDROID_ABI=${ANDROID_ARCH_ABI} \
-DANDROID_ARM_MODE=${ARCH} \
-DANDROID_PLATFORM=android-${ANDROID_VERSION} \
-DANDROID_TOOLCHAIN=clang \
-DCMAKE_TOOLCHAIN_FILE=${NDK_PATH}/build/cmake/android.toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DANDROID_NDK=${NDK_PATH} \
-DCMAKE_POSITION_INDEPENDENT_CODE=1 \
-DCMAKE_INSTALL_PREFIX=${PREFIX} \
-DANDROID_ARM_NEON=TRUE \
-DANDROID_STL=c++_static \
-DCMAKE_C_FLAGS="${CFALGS} -Os -Wall -pipe -fPIC" \
-DCMAKE_CXX_FLAGS="${CFALGS} -Os -Wall -pipe -fPIC" \
-DANDROID_CPP_FEATURES=rtti exceptions \
-DWITH_JPEG8=1 \
..
# 生成 so 庫
make clean
make
make install
複製代碼
將咱們上面編譯好的 so 和頭文件拷貝到咱們的項目中
在 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
......
)
複製代碼
由於咱們只編譯了 armeabi 架構的 so, 所以咱們須要再 gradle 中添加 filters
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
externalNativeBuild {
ndk {
abiFilters "armeabi-v7a"
}
}
}
}
複製代碼
好的, 至此咱們的集成就完成了, 接下來提供一些簡單的用法
咱們編譯 libjpeg-turbo 的主要目的就是爲了進行 JPEG 的高質量壓縮, 關於 libjpeg-turbo 的使用, 這裏就不贅述了, 其官方提供好的 sample 以下
raw.githubusercontent.com/libjpeg-tur…
簡單的來講, 就是將 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;
LOGI("Bitmap width is %d, height is %d", cols, rows);
// 若不爲 ARGB8888, 則不給予壓縮
if (format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
LOGE("Unsupported Bitmap channels, Please ensure channels is ARGB_8888.");
return false;
}
// 2. 解析數據
LOGI("Parse bitmap pixels");
// 鎖定畫布
uchar *pixels = NULL;
AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
if (pixels == NULL) {
LOGE("Fetch Bitmap data failed.");
return false;
}
// 建立存儲數組
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) {
// 2.1 獲取像素值
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 通道值
pixels += 4;
// 2.2 爲 Data 填充數據
*(data++) = b;
*(data++) = g;
*(data++) = r;
}
}
// 解鎖畫布
AndroidBitmap_unlockPixels(env, bitmap);
// 3. 使用 libjpeg 進行圖片質量壓縮
LOGI("Lib jpeg turbo do compress");
char *output_filename = (char *) (env)->GetStringUTFChars(destPath_, NULL);
int result = LibJpegTurboUtils::write_JPEG_file(data_header_pointer, rows, cols,
output_filename,
quality);
// 4. 釋放資源
LOGI("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 版本的困擾, 感興趣的同窗能夠按照上述的方式試試看。