Android 平台 WebP 图片编码/解码的 native 实现

这两天公司在评估将图片切换为 WebP 格式,以节省带宽。

说实话,之前对 WebP 知之甚少,之前开发 Android ,只做过 SVG 相关的处理。

WebP 是 Google 提出的一种替换 png 和 bmp 的图片格式,根据 Google IO 2013 上的介绍,JPEG / PNG 转换为 WebP 格式后,文件大小平均可以减少 30% ,而加载时间可以减少三分之一。

WebP 简介

WebM 是一种使用 VP8 编解码器的视频格式,而 WebP 是从 VP8 中提取而来。

我们知道,对图片文件解码后,其头部一般会包含文件类型、版本信息、尺寸大小等相关信息,WebP 的头部信息如下:

+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      'R'      |      'I'      |      'F'      |      'F'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           File Size                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|      'W'      |      'E'      |      'B'      |      'P'      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 1 ~ 4 字节:分别是 RIFF 四个字符的 ASCII 码值( WebP 文件格式是基于 RIFF 文档格式的);

  • 5 ~ 6 字节:图片的宽高;

  • 7 ~ 8 字节:保留字段,都是 0

  • 9 ~ 12 字节:分别是 WEBP 四个字符的 ASCII 码值;

通过对文件解码后的字节数流前 12 个字节进行分析,可以确定是否是采用 WebP 编码格式,以帮助我们选择图片的呈现方案。更多 WebP 编码信息详见 Google Developers 相关页面

WebP/M 都是基于开源的 Chromium 项目,Android 从 4.0 开始才支持 WebP 格式,详见 Google 官方文档,而 Chrome 浏览器也是从 Android 4.0 才开始支持。

因此不管是浏览器端呈现还是 APP 直接呈现,Android 4.0 之前的版本都不能直接处理 WebP。

编译 WebP 类库

先看看上面 Google IO 2013 推荐的一个开源项目

下载后进入 jni 目录执行 ndk-build 的时候提示找不到 webp 目录下的文件,而进入该目录发现确实是空的,坑爹啊,只有自己去 Chromium 的 webp 目录下面去找了。

下载后,将根目录下的 decdsp encutils 目录、Makefile.amMakefile.in 文件,以及 src\webp 目录下的 decode_vp8.hdecode.hencode.htypes.h 等文件全部拷贝过来,并修改 jni 根目录下的编译文件 Android.mk ,再次执行 ndk-build 即可:

LOCAL_PATH:= $(call my-dir)
# WebP library
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \
	src/dec/alpha.c \
	src/dec/frame.c \
	src/dec/idec.c \
	src/dec/layer.c \
	src/dec/quant.c \
	src/dec/tree.c \
	src/dec/vp8.c \
	src/dec/webp.c \
	src/dec/io.c \
	src/dec/buffer.c \
	src/dsp/yuv.c \
	src/dsp/upsampling.c \
	src/dsp/cpu.c \
	src/dsp/dec.c \
	src/dsp/dec_neon.c \
	src/dsp/enc.c \
	src/enc/alpha.c \
	src/enc/analysis.c \
	src/enc/config.c \
	src/enc/cost.c \
	src/enc/filter.c \
	src/enc/frame.c \
	src/enc/iterator.c \
	src/enc/layer.c \
	src/enc/picture.c \
	src/enc/quant.c \
	src/enc/syntax.c \
	src/enc/tree.c \
	src/enc/webpenc.c \
	src/utils/bit_reader.c \
	src/utils/bit_writer.c \
	src/utils/thread.c
LOCAL_CFLAGS := -Wall -DANDROID -DHAVE_MALLOC_H -DHAVE_PTHREAD -DWEBP_USE_THREAD \
                -finline-functions -frename-registers -ffast-math \
                -s -fomit-frame-pointer -Isrc/webp   
LOCAL_C_INCLUDES += $(LOCAL_PATH)/src
#LOCAL_EXPORT_C_INCLUDES += $(LOCAL_PATH)/src
LOCAL_MODULE:= webp
include $(BUILD_STATIC_LIBRARY)
# My Library 
include $(CLEAR_VARS)
LOCAL_MODULE    := WebP-Android
LOCAL_SRC_FILES := \
	com_rincliu_webp.cpp \
	com_rincliu_webp_WebPFactory.cpp
LOCAL_CFLAGS := -Wall -DANDROID
LOCAL_STATIC_LIBRARIES := webp
#LOCAL_C_INCLUDES := $(LOCAL_PATH)/src/webp
LOCAL_LDLIBS    := -lm -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)
#	src/dec/dsp.c \
#	src/enc/dsp.c \

这些 .cpp.h 文件中的 include 路径也可能需要修改,具体看你存放的相对路径。

关键代码

由于 WebP 和大多数开源类库一样采用 native 语言 C 编写,因此 Android 这边 Java 语言要想调用必须使用 JNI 。这里主要提供了两个方法:nativeEncodeBitmap()nativeDecodeByteArray() ,一个编码一个解码。

native 关键代码:

JNIEXPORT jobject JNICALL Java_com_rincliu_webp_WebPFactory_nativeDecodeByteArray
  (JNIEnv *jniEnv, jclass, jbyteArray byteArray, jobject options){
	// Check if input is valid
	if(!byteArray){
		jniEnv->ThrowNew(jrefs::java::lang::NullPointerException->jclassRef, "Input buffer can not be null");
		return 0;
	}
	// Log what version of WebP is used
	__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Using WebP Decoder %08x", WebPGetDecoderVersion());
	// Lock buffer
	jbyte* inputBuffer = jniEnv->GetByteArrayElements(byteArray, NULL);
	size_t inputBufferLen = jniEnv->GetArrayLength(byteArray);
	// Validate image
	int bitmapWidth = 0;
	int bitmapHeight = 0;
	if(!WebPGetInfo((uint8_t*)inputBuffer, inputBufferLen, &bitmapWidth, &bitmapHeight)){
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Invalid WebP format");
		return 0;
	}
	// Check if size is all what we were requested to do
	if(options && jniEnv->GetBooleanField(options, jrefs::android::graphics::BitmapFactory->Options.inJustDecodeBounds) == JNI_TRUE){
		// Set values
		jniEnv->SetIntField(options, jrefs::android::graphics::BitmapFactory->Options.outWidth, bitmapWidth);
		jniEnv->SetIntField(options, jrefs::android::graphics::BitmapFactory->Options.outHeight, bitmapHeight);
		// Unlock buffer
		jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
		return 0;
	}
	__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Decoding %dx%d bitmap", bitmapWidth, bitmapHeight);
	// Create bitmap
	jobject value__ARGB_8888 = jniEnv->GetStaticObjectField(jrefs::android::graphics::Bitmap->Config.jclassRef, jrefs::android::graphics::Bitmap->Config.ARGB_8888);
	jobject outputBitmap = jniEnv->CallStaticObjectMethod(jrefs::android::graphics::Bitmap->jclassRef, jrefs::android::graphics::Bitmap->createBitmap,
		(jint)bitmapWidth, (jint)bitmapHeight,
		value__ARGB_8888);
	if(!outputBitmap){
		jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to allocate Bitmap");
		return 0;
	}
	outputBitmap = jniEnv->NewLocalRef(outputBitmap);
	// Get information about bitmap passed
	AndroidBitmapInfo bitmapInfo;
	if(AndroidBitmap_getInfo(jniEnv, outputBitmap, &bitmapInfo) != ANDROID_BITMAP_RESUT_SUCCESS){
		jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
		jniEnv->DeleteLocalRef(outputBitmap);
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to get Bitmap information");
		return 0;
	}
	// Lock pixels
	void* bitmapPixels = 0;
	if(AndroidBitmap_lockPixels(jniEnv, outputBitmap, &bitmapPixels) != ANDROID_BITMAP_RESUT_SUCCESS){
		jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
		jniEnv->DeleteLocalRef(outputBitmap);
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to lock Bitmap pixels");
		return 0;
	}
	// Decode to ARGB
	if(!WebPDecodeRGBAInto((uint8_t*)inputBuffer, inputBufferLen, (uint8_t*)bitmapPixels, bitmapInfo.height * bitmapInfo.stride, bitmapInfo.stride)){
		AndroidBitmap_unlockPixels(jniEnv, outputBitmap);
		jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
		jniEnv->DeleteLocalRef(outputBitmap);
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to unlock Bitmap pixels");
		return 0;
	}
	// Unlock pixels
	if(AndroidBitmap_unlockPixels(jniEnv, outputBitmap) != ANDROID_BITMAP_RESUT_SUCCESS){
		jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
		jniEnv->DeleteLocalRef(outputBitmap);
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to unlock Bitmap pixels");
		return 0;
	}
	// Unlock buffer
	jniEnv->ReleaseByteArrayElements(byteArray, inputBuffer, JNI_ABORT);
	return outputBitmap;
}

JNIEXPORT jbyteArray JNICALL Java_com_rincliu_webp_WebPFactory_nativeEncodeBitmap
  (JNIEnv * jniEnv, jclass, jobject bitmap, jint quality){
	// Check if input is valid
	if(!bitmap){
		jniEnv->ThrowNew(jrefs::java::lang::NullPointerException->jclassRef, "Bitmap can not be null");
		return 0;
	}
	// Get information about bitmap passed
	AndroidBitmapInfo bitmapInfo;
	if(AndroidBitmap_getInfo(jniEnv, bitmap, &bitmapInfo) != ANDROID_BITMAP_RESUT_SUCCESS){
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to get Bitmap information");
		return 0;
	}
	// Check for format
	if(bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888 && bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGB_565){
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Unsupported Bitmap configuration. Currently only RGBA_8888 and RGB_565 are supported");
		return 0;
	}
	// Log what version of WebP is used
	__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Using WebP Encoder %08x", WebPGetEncoderVersion());
	// Lock pixels
	void* bitmapPixels = 0;
	if(AndroidBitmap_lockPixels(jniEnv, bitmap, &bitmapPixels) != ANDROID_BITMAP_RESUT_SUCCESS){
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to lock Bitmap pixels");
		return 0;
	}
	// Convert color space
	const uint8_t* src = (uint8_t*)bitmapPixels;
	const uint32_t src_stride = bitmapInfo.stride;
	const uint32_t dst_stride = GetDestinationScanlinePixelByteSize(bitmapInfo.format) * bitmapInfo.width;
	const ScanlineImporter scanline_import = ChooseImporter(bitmapInfo.format);
	uint8_t* dst = new uint8_t[dst_stride * bitmapInfo.height];
	for (int y = 0; y < bitmapInfo.height; ++y){
		scanline_import(src + y * src_stride, dst + y * dst_stride, bitmapInfo.width);
	}
	// Unlock pixels
	if(AndroidBitmap_unlockPixels(jniEnv, bitmap) != ANDROID_BITMAP_RESUT_SUCCESS){
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to unlock Bitmap pixels");
		return 0;
	}
	// Encode and save
	size_t encodedImageSize = 0;
	uint8_t* encodedImageData = 0;
	if(bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888){
		__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Encoding %dx%d image as RGBA_8888", bitmapInfo.width, bitmapInfo.height);
		encodedImageSize = WebPEncodeRGBA(dst, bitmapInfo.width, bitmapInfo.height, dst_stride, quality, &encodedImageData);
	}else if(bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGB_565){
		__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "Encoding %dx%d image as RGBA_565", bitmapInfo.width, bitmapInfo.height);
		encodedImageSize = WebPEncodeRGB(dst, bitmapInfo.width, bitmapInfo.height, dst_stride, quality, &encodedImageData);
	}
	delete[] dst;
	if(encodedImageSize == 0){
		jniEnv->ThrowNew(jrefs::java::lang::RuntimeException->jclassRef, "Failed to encode to WebP");
		return 0;
	}
	// Copy to output buffer
	jbyteArray resultArray = jniEnv->NewByteArray(encodedImageSize);
	jbyte* resultArrayBuffer = jniEnv->GetByteArrayElements(resultArray, NULL);
	memcpy(resultArrayBuffer, encodedImageData, encodedImageSize);
	jniEnv->ReleaseByteArrayElements(resultArray, resultArrayBuffer, 0);
	__android_log_print(ANDROID_LOG_INFO, LOG_TAG, "WebP image size %d bytes", encodedImageSize);
	// Free
	free(encodedImageData);
	return resultArray;
}

Java 关键代码:

// Load library
static{
	System.loadLibrary("WebP-Android");
}

/**
 * Decodes byte array to bitmap 
 * @param data Byte array with WebP bitmap data
 * @param opts Options to control decoding. Accepts null
 * @return Decoded bitmap
 */
public static native Bitmap nativeDecodeByteArray(byte[] data, BitmapFactory.Options options);
	
/**
 * Encodes bitmap into byte array
 * @param bitmap Bitmap
 * @param quality Quality, should be between 0 and 100
 * @return Encoded byte array
 */
public static native byte[] nativeEncodeBitmap(Bitmap bitmap, int quality);

/**
 * Verifies bitmap's format
 * @param data
 * @return
 */
public static boolean isWebP(byte[] data){
	return data != null && data.length > 12 
		&& data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' 
		&& data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P';
}

完整项目代码详见我的 GitHub 开源项目 Roid-WebP

另外,由于 Android 4.0 以上原生支持 WebP ,可以判断系统版本,只在 4.0 以下使用这个 native 库,示例代码:

public Bitmap decodeImageBytes(byte[] data, BitmapFactory.Options optns){
	return VERSION.SDK_INT<VERSION_CODES.ICE_CREAM_SANDWICH ?
		WebPFactory.nativeDecodeByteArray(data, optns) 
		: BitmapFactory.decodeByteArray(data, 0, data.length, optns);
}