diff --git a/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/WebUtilsTest.kt b/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/WebUtilsTest.kt new file mode 100644 index 0000000..1ce0cef --- /dev/null +++ b/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/WebUtilsTest.kt @@ -0,0 +1,69 @@ +package network.loki.messenger.libsession_util.image + +import android.graphics.ImageDecoder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.sessionfoundation.libsession_util.test.R + +@RunWith(AndroidJUnit4::class) +class WebUtilsTest { + @Test + fun testReencodeWebP() { + + for (outputSize in listOf(200, 400, 600)) { + val output = InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.webp_animated).use { input -> + WebPUtils.reencodeWebPAnimation( + input = input.readAllBytes(), + timeoutMills = 100_000L, + targetWidth = outputSize, + targetHeight = outputSize + ) + } + + ImageDecoder.decodeDrawable( + ImageDecoder.createSource(output) + ) { decoder, info, source -> + assertEquals(outputSize, info.size.width) + assertEquals(outputSize, info.size.height) + assertTrue(info.isAnimated) + assertEquals("image/webp", info.mimeType) + } + } + } + + @Test + fun testEncodeGIFToWebP() { + for (outputSize in listOf(200, 400, 600)) { + val output = InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.earth).use { input -> + WebPUtils.encodeGifToWebP( + input = input, + timeoutMills = 100_000L, + targetWidth = outputSize, + targetHeight = outputSize + ) + } + + ImageDecoder.decodeDrawable( + ImageDecoder.createSource(output) + ) { decoder, info, source -> + assertEquals("image/webp", info.mimeType) + assertEquals(outputSize, info.size.width) + assertEquals(outputSize, info.size.height) + assertTrue(info.isAnimated) + } + } + } +} \ No newline at end of file diff --git a/library/src/androidTest/res/raw/webp_animated.webp b/library/src/androidTest/res/raw/webp_animated.webp new file mode 100644 index 0000000..8d4a214 Binary files /dev/null and b/library/src/androidTest/res/raw/webp_animated.webp differ diff --git a/library/src/main/cpp/gif_utils.cpp b/library/src/main/cpp/gif_utils.cpp index ba6a174..a7cbdb1 100644 --- a/library/src/main/cpp/gif_utils.cpp +++ b/library/src/main/cpp/gif_utils.cpp @@ -26,7 +26,8 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv * JniInputStream input_stream(env, input); EasyGifReader decoder = EasyGifReader::openCustom([](void *out_buffer, size_t size, void *ctx) { - return reinterpret_cast(ctx)->read(reinterpret_cast(out_buffer), size); + reinterpret_cast(ctx)->read_fully(reinterpret_cast(out_buffer), size); + return size; }, &input_stream); std::vector output_buffer; @@ -100,7 +101,7 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv * libyuv::kFilterBox ); - // Convert the scaled ARGB32 back to RGB24 for encoding + // Convert the scaled ARGB32 back to RGBA for encoding libyuv::ARGBToRGBA( encode_argb_buffer.data(), target_width * 4, encode_rgba_buffer.data(), target_width * 4, @@ -139,12 +140,12 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_isAnimatedGif(JNIEnv EasyGifReader decoder = EasyGifReader::openCustom( [](void *out_buffer, size_t size, void *ctx) { - return reinterpret_cast(ctx)->read( - reinterpret_cast(out_buffer), size); + reinterpret_cast(ctx)->read_fully(reinterpret_cast(out_buffer), size); + return size; }, &input_stream); return decoder.frameCount() > 1; - } catch (...) { + } catch (const EasyGifReader::Error &e) { // Is there's a java exception pending? if (env->ExceptionCheck()) { return false; diff --git a/library/src/main/cpp/jni_input_stream.h b/library/src/main/cpp/jni_input_stream.h index ec4a09f..d434bbb 100644 --- a/library/src/main/cpp/jni_input_stream.h +++ b/library/src/main/cpp/jni_input_stream.h @@ -32,6 +32,17 @@ class JniInputStream { return bytes_read; } + + void read_fully(uint8_t *buffer, size_t size) { + size_t total_bytes_read = 0; + while (total_bytes_read < size) { + size_t bytes_read = read(buffer + total_bytes_read, size - total_bytes_read); + if (bytes_read == 0) { + throw std::runtime_error("EOF reached"); + } + total_bytes_read += bytes_read; + } + } }; #endif //LIBSESSION_UTIL_ANDROID_JNI_INPUT_STREAM_H diff --git a/library/src/main/cpp/webp_utils.cpp b/library/src/main/cpp/webp_utils.cpp index a357730..13b0a23 100644 --- a/library/src/main/cpp/webp_utils.cpp +++ b/library/src/main/cpp/webp_utils.cpp @@ -1,5 +1,6 @@ #include #include "jni_utils.h" +#include "jni_input_stream.h" #include @@ -8,6 +9,8 @@ #include #include +#include + template using WebPPtr = std::unique_ptr; @@ -123,3 +126,106 @@ Java_network_loki_messenger_libsession_1util_image_WebPUtils_reencodeWebPAnimati return out_ref.java_array(); } + + +static void destroyWebPPicture(WebPPicture* pic) { + WebPPictureFree(pic); + delete pic; +} + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_image_WebPUtils_encodeGifToWebP(JNIEnv *env, + jobject thiz, + jobject input, + jlong timeout_mills, + jint target_width, + jint target_height) { + return jni_utils::run_catching_cxx_exception_or_throws(env, [=]() -> jbyteArray { + JniInputStream input_stream(env, input); + + const auto deadline = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(timeout_mills); + + auto is_timeout = [&]() { + if (std::chrono::high_resolution_clock::now() > deadline) { + env->ThrowNew(env->FindClass("java/util/concurrent/TimeoutException"), + "GIF re-encoding timed out"); + return true; + } + return false; + }; + + EasyGifReader decoder = EasyGifReader::openCustom([](void *out_buffer, size_t size, void *ctx) { + reinterpret_cast(ctx)->read_fully(reinterpret_cast(out_buffer), size); + return size; + }, &input_stream); + + WebPPtr encoder(WebPAnimEncoderNew(target_width, target_height, nullptr), &WebPAnimEncoderDelete); + if (!encoder) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to create animation encoder"); + return 0; + } + + std::vector decode_argb_buffer(decoder.width() * decoder.height() * 4); + std::vector encode_argb_buffer; + int frame_delay_ms = 0; + + WebPPtr pic(new WebPPicture, &destroyWebPPicture); + WebPPictureInit(pic.get()); + pic->use_argb = 1; + + for (auto frame = decoder.begin(); frame != decoder.end() && !is_timeout(); ++frame) { + // Import the frame into a WebPPicture + pic->width = decoder.width(); + pic->height = decoder.height(); + pic->argb_stride = decoder.width(); + + if (!WebPPictureImportRGBA(pic.get(), frame->pixels(), decoder.width() * 4)) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to import frame into picture"); + return nullptr; + } + + // If the target size is different, rescale the frame + if (target_width != decoder.width() || target_height != decoder.height()) { + if (!WebPPictureRescale(pic.get(), target_width, target_height)) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to rescale picture"); + return nullptr; + } + } + + // Now we have a WebPPicture ready to be added to the encoder + WebPConfig encode_config; + WebPConfigInit(&encode_config); + encode_config.quality = 95.f; + + frame_delay_ms += frame.duration().milliseconds(); + + auto encode_succeeded = WebPAnimEncoderAdd(encoder.get(), pic.get(), + frame_delay_ms, &encode_config); + + if (!encode_succeeded) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + WebPAnimEncoderGetError(encoder.get())); + return nullptr; + } + } + + WebPData out; + WebPDataInit(&out); + + if (!WebPAnimEncoderAssemble(encoder.get(), &out)) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to assemble animation"); + return nullptr; + } + + jni_utils::JavaByteArrayRef out_ref(env, env->NewByteArray(out.size)); + std::memcpy(out_ref.bytes(), out.bytes, out.size); + WebPDataClear(&out); + + return out_ref.java_array(); + }); +} \ No newline at end of file diff --git a/library/src/main/java/network/loki/messenger/libsession_util/image/WebPUtils.kt b/library/src/main/java/network/loki/messenger/libsession_util/image/WebPUtils.kt index 07976b2..fcf338b 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/image/WebPUtils.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/image/WebPUtils.kt @@ -1,6 +1,9 @@ package network.loki.messenger.libsession_util.image -object WebPUtils { +import network.loki.messenger.libsession_util.LibSessionUtilCApi +import java.io.InputStream + +object WebPUtils : LibSessionUtilCApi() { /** * Re-encode the webP animation, resizing each frame to scale to the target width and height. * This can serve two purposes: @@ -18,4 +21,11 @@ object WebPUtils { targetWidth: Int, targetHeight: Int, ): ByteArray + + external fun encodeGifToWebP( + input: InputStream, + timeoutMills: Long, + targetWidth: Int, + targetHeight: Int, + ): ByteArray } \ No newline at end of file