Enable WebP decoding in Signal using libwebp v1.3.2

Co-authored-by: Greyson Parrelli <greyson@signal.org>
Co-authored-by: Greyson Parrelli <greyson@pop-os.localdomain>
This commit is contained in:
Cody Henthorne
2023-09-22 12:55:49 -04:00
committed by Alex Hart
parent 091f7c49ab
commit a7d9fd19d9
59 changed files with 874 additions and 7 deletions

View File

@@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.22.1)
project(signal-webp)
add_compile_options(-Oz)
set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)
set(BUILD_SHARED_LIBS ON)
set(WEBP_BUILD_ANIM_UTILS OFF)
set(WEBP_BUILD_CWEBP OFF)
set(WEBP_BUILD_DWEBP OFF)
set(WEBP_BUILD_VWEBP OFF)
set(WEBP_BUILD_WEBPINFO OFF)
set(WEBP_BUILD_LIBWEBPMUX OFF)
set(WEBP_BUILD_EXTRAS OFF)
set(WEBP_NEAR_LOSSLESS, OFF)
file(GLOB SOURCES ${CMAKE_SOURCE_DIR}/*.cpp)
add_subdirectory(${LIBWEBP_PATH} ${CMAKE_CURRENT_BINARY_DIR}/libwebp)
add_library(signalwebp SHARED ${SOURCES})
target_include_directories(signalwebp PRIVATE ${LIBWEBP_PATH}/src)
target_link_libraries(signalwebp webpdemux)

View File

@@ -0,0 +1,62 @@
#include <jni.h>
#include <webp/demux.h>
jobject createBitmap(JNIEnv *env, int width, int height, const uint8_t *pixels) {
static auto jbitmapConfigClass = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("android/graphics/Bitmap$Config")));
static jfieldID jbitmapConfigARGB8888Field = env->GetStaticFieldID(jbitmapConfigClass, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");
static auto jbitmapClass = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("android/graphics/Bitmap")));
static jmethodID jbitmapCreateBitmapMethod = env->GetStaticMethodID(jbitmapClass, "createBitmap", "([IIIIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jintArray intArray = env->NewIntArray(width * height);
env->SetIntArrayRegion(intArray, 0, width * height, reinterpret_cast<const jint *>(pixels));
jobject argb8888Config = env->GetStaticObjectField(jbitmapConfigClass, jbitmapConfigARGB8888Field);
jobject jbitmap = env->CallStaticObjectMethod(jbitmapClass, jbitmapCreateBitmapMethod, intArray, 0, width, width, height, argb8888Config);
env->DeleteLocalRef(argb8888Config);
return jbitmap;
}
jobject nativeDecodeBitmap(JNIEnv *env, jobject, jbyteArray data) {
jbyte *javaBytes = env->GetByteArrayElements(data, nullptr);
auto *buffer = reinterpret_cast<uint8_t *>(javaBytes);
jsize bufferLength = env->GetArrayLength(data);
WebPBitstreamFeatures features;
WebPGetFeatures(buffer, bufferLength, &features);
int width;
int height;
uint8_t *pixels = WebPDecodeBGRA(buffer, bufferLength, &width, &height);
jobject jbitmap = createBitmap(env, width, height, pixels);
WebPFree(pixels);
env->ReleaseByteArrayElements(data, javaBytes, 0);
return jbitmap;
}
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env;
if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass c = env->FindClass("org/signal/glide/webp/WebpDecoder");
if (c == nullptr) {
return JNI_ERR;
}
static const JNINativeMethod methods[] = {
{"nativeDecodeBitmap", "([B)Landroid/graphics/Bitmap;", reinterpret_cast<void *>(nativeDecodeBitmap)}
};
int rc = env->RegisterNatives(c, methods, sizeof(methods) / sizeof(JNINativeMethod));
if (rc != JNI_OK) {
return rc;
}
return JNI_VERSION_1_6;
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp
import android.graphics.Bitmap
class WebpDecoder {
init {
System.loadLibrary("signalwebp")
}
external fun nativeDecodeBitmap(data: ByteArray): Bitmap?
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp
import android.graphics.Bitmap
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapResource
import org.signal.core.util.StreamUtil
import org.signal.core.util.logging.Log
import java.io.IOException
import java.io.InputStream
class WebpInputStreamResourceDecoder(private val bitmapPool: BitmapPool) : ResourceDecoder<InputStream, Bitmap> {
companion object {
private const val TAG = "WebpResourceDecoder" // Name > 23 characters
private val MAGIC_NUMBER_P1 = byteArrayOf(0x52, 0x49, 0x46, 0x46) // "RIFF"
private val MAGIC_NUMBER_P2 = byteArrayOf(0x57, 0x45, 0x42, 0x50) // "WEBP"
private const val MAX_WEBP_COMPRESSED_SIZE = 10 * 1024 * 1024; // 10mb
}
/**
* The "magic number" for a WEBP file is in the first 12 bytes. The layout is:
*
* [0-3]: "RIFF"
* [4-7]: File length
* [8-11]: "WEBP"
*
* We're not verifying the file length here, so we just need to check the first and last
*/
override fun handles(source: InputStream, options: Options): Boolean {
return try {
val magicNumberP1 = ByteArray(4)
StreamUtil.readFully(source, magicNumberP1)
val fileLength = ByteArray(4)
StreamUtil.readFully(source, fileLength)
val magicNumberP2 = ByteArray(4)
StreamUtil.readFully(source, magicNumberP2)
magicNumberP1.contentEquals(MAGIC_NUMBER_P1) && magicNumberP2.contentEquals(MAGIC_NUMBER_P2)
} catch (e: IOException) {
Log.w(TAG, "Failed to read magic number from stream!", e)
false
}
}
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<Bitmap>? {
Log.d(TAG, "decode()")
val webp: ByteArray = try {
StreamUtil.readFully(source, MAX_WEBP_COMPRESSED_SIZE)
} catch (e: IOException) {
Log.w(TAG, "Unexpected IOException hit while reading image data", e)
throw e
}
val bitmap: Bitmap? = WebpDecoder().nativeDecodeBitmap(webp)
return BitmapResource.obtain(bitmap, bitmapPool)
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.webp
import android.content.Context
import android.graphics.Bitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.module.LibraryGlideModule
import org.signal.core.util.logging.Log
import java.io.InputStream
/**
* Registers a custom handler for webp images.
*/
@GlideModule
class WebpLibraryGlideModule : LibraryGlideModule() {
companion object {
private val TAG = Log.tag(WebpLibraryGlideModule::class.java)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
Log.d(TAG, "registerComponents()")
registry.prepend(InputStream::class.java, Bitmap::class.java, WebpInputStreamResourceDecoder(glide.bitmapPool))
}
}