diff --git a/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/GifUtilsTest.kt b/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/GifUtilsTest.kt index b2b4125..51b9a45 100644 --- a/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/GifUtilsTest.kt +++ b/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/GifUtilsTest.kt @@ -22,7 +22,7 @@ class GifUtilsTest { .resources .openRawResource(R.raw.earth).use { input -> GifUtils.reencodeGif( - input = input, + input = input.readAllBytes(), timeoutMills = 100_000L, targetWidth = outputSize, targetHeight = outputSize @@ -50,6 +50,17 @@ class GifUtilsTest { .use(GifUtils::isAnimatedGif) ) + assertTrue( + InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.earth) + .use { + GifUtils.isAnimatedGif(it.readBytes()) + } + ) + assertFalse( InstrumentationRegistry.getInstrumentation() .targetContext @@ -58,5 +69,16 @@ class GifUtilsTest { .openRawResource(R.raw.sunflower_noanim) .use(GifUtils::isAnimatedGif) ) + + assertFalse( + InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.sunflower_noanim) + .use { + GifUtils.isAnimatedGif(it.readBytes()) + } + ) } } \ No newline at end of file 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 index 1ce0cef..1f856dd 100644 --- 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 @@ -49,7 +49,7 @@ class WebUtilsTest { .resources .openRawResource(R.raw.earth).use { input -> WebPUtils.encodeGifToWebP( - input = input, + input = input.readAllBytes(), timeoutMills = 100_000L, targetWidth = outputSize, targetHeight = outputSize diff --git a/library/src/main/cpp/gif_utils.cpp b/library/src/main/cpp/gif_utils.cpp index a7cbdb1..f42a86a 100644 --- a/library/src/main/cpp/gif_utils.cpp +++ b/library/src/main/cpp/gif_utils.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -18,17 +19,14 @@ using GifPtr = std::unique_ptr; extern "C" JNIEXPORT jbyteArray JNICALL Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv *env, jobject thiz, - jobject input, + jbyteArray 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); + jni_utils::JavaByteArrayRef input_ref(env, input); - 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); + EasyGifReader decoder = EasyGifReader::openMemory(input_ref.bytes(), input_ref.size()); std::vector output_buffer; @@ -58,14 +56,6 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv * const auto needRescale = (src_width != target_width) || (src_height != target_height); 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; - }; std::vector decode_argb_buffer, encode_argb_buffer, encode_rgba_buffer; if (needRescale) { @@ -74,7 +64,7 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv * encode_rgba_buffer.resize(target_width * target_height * 4); } - for (auto frame = decoder.begin(); frame != decoder.end() && !is_timeout(); ++frame) { + for (auto frame = decoder.begin(); frame != decoder.end(); ++frame) { // Here we would add the frame to the encoder CGIFrgb_FrameConfig config = { .pImageData = const_cast(frame->pixels()), @@ -112,13 +102,15 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv * config.pImageData = encode_rgba_buffer.data(); } - if (is_timeout()) { - return nullptr; - } - if (cgif_rgb_addframe(encoder.get(), &config) != CGIF_OK) { throw std::runtime_error("Failed to encode GIF frame"); } + + if (std::chrono::high_resolution_clock::now() > deadline) { + env->ThrowNew(env->FindClass("java/util/concurrent/TimeoutException"), + "GIF re-encoding timed out"); + return nullptr; + } } // Close the encoder to finalize the GIF @@ -130,29 +122,119 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv * }); } +static bool isAnimatedGif(GifFileType *gif_file) { + GifRecordType record_type; + std::vector line_buf; + int image_count = 0; + + while (DGifGetRecordType(gif_file, &record_type) == GIF_OK) { + switch (record_type) { + case IMAGE_DESC_RECORD_TYPE: { + if (DGifGetImageDesc(gif_file) != GIF_OK) { + throw std::runtime_error("Failed to read GIF image descriptor"); + } + + image_count++; + + if (image_count > 1) { + return true; + } + + line_buf.resize(gif_file->Image.Width); + for (int i = 0; i < gif_file->Image.Height; ++i) { + if (DGifGetLine(gif_file, line_buf.data(), gif_file->Image.Width) != GIF_OK) { + throw std::runtime_error("Failed to read GIF image line"); + } + } + break; + } + + case SCREEN_DESC_RECORD_TYPE: { + if (DGifGetScreenDesc(gif_file) != GIF_OK) { + throw std::runtime_error("Failed to read GIF screen descriptor"); + } + + break; + } + + case TERMINATE_RECORD_TYPE: return false; + + case EXTENSION_RECORD_TYPE: { + GifByteType *e = nullptr; + int ext_code; + if (DGifGetExtension(gif_file, &ext_code, &e) != GIF_OK) { + throw std::runtime_error("Failed to read GIF extension"); + } + + while (e) { + DGifGetExtensionNext(gif_file, &e); + } + + break; + } + + default: break; + } + } + + return false; +} + extern "C" JNIEXPORT jboolean JNICALL -Java_network_loki_messenger_libsession_1util_image_GifUtils_isAnimatedGif(JNIEnv *env, jobject thiz, - jobject input) { +Java_network_loki_messenger_libsession_1util_image_GifUtils_isAnimatedGifForStream(JNIEnv *env, jobject thiz, + jobject input) { return jni_utils::run_catching_cxx_exception_or_throws(env, [=]() { JniInputStream input_stream(env, input); - try { - - 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); - - return decoder.frameCount() > 1; - } catch (const EasyGifReader::Error &e) { - // Is there's a java exception pending? - if (env->ExceptionCheck()) { - return false; - } - // Otherwise, decoding error means we don't have a valid GIF - return false; + int error = D_GIF_SUCCEEDED; + GifPtr gif_file(DGifOpen( + &input_stream, + [](GifFileType *f, GifByteType *out_data, int size) -> int { + return reinterpret_cast(f->UserData)->read_fully(out_data, size); + }, + &error + ), + [](GifFileType *ptr) { DGifCloseFile(ptr, nullptr); } + ); + + if (!gif_file) { + throw std::runtime_error(("Failed to open GIF for reading: " + std::to_string(error)).c_str()); } + + return isAnimatedGif(gif_file.get()); + }); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_image_GifUtils_isAnimatedGifForBytes(JNIEnv *env, + jobject thiz, + jbyteArray input) { + return jni_utils::run_catching_cxx_exception_or_throws(env, [=]() { + jni_utils::JavaByteArrayRef input_ref(env, input); + auto input_data = input_ref.get(); + + int error = D_GIF_SUCCEEDED; + GifPtr gif_file(DGifOpen( + &input_data, + [](GifFileType *f, GifByteType *out_data, int size) -> int { + auto input = reinterpret_cast *>(f->UserData); + auto copy_count = std::min(input->size(), static_cast(size)); + std::memcpy(out_data, input->data(), copy_count); + *input = input->subspan(copy_count); + return copy_count; + }, + &error + ), + [](GifFileType *ptr) { DGifCloseFile(ptr, nullptr); } + ); + + if (!gif_file) { + throw std::runtime_error(("Failed to open GIF for reading: " + std::to_string(error)).c_str()); + } + + return isAnimatedGif(gif_file.get()); }); + } \ No newline at end of file diff --git a/library/src/main/cpp/jni_input_stream.h b/library/src/main/cpp/jni_input_stream.h index d434bbb..f199100 100644 --- a/library/src/main/cpp/jni_input_stream.h +++ b/library/src/main/cpp/jni_input_stream.h @@ -10,38 +10,44 @@ class JniInputStream { JNIEnv *env; jobject input_stream; jmethodID read_method; + std::optional> buffer; public: JniInputStream(JNIEnv *env, jobject input_stream) : env(env), input_stream(input_stream) { jni_utils::JavaLocalRef clazz(env, env->GetObjectClass(input_stream)); - read_method = env->GetMethodID(clazz.get(), "read", "([B)I"); + read_method = env->GetMethodID(clazz.get(), "read", "([BII)I"); } - size_t read(uint8_t *buffer, size_t size) { - jni_utils::JavaLocalRef byte_array(env, env->NewByteArray(static_cast(size))); - jint bytes_read = env->CallIntMethod(input_stream, read_method, byte_array.get()); + JniInputStream(const JniInputStream&) = delete; + JniInputStream& operator=(const JniInputStream&) = delete; - if (env->ExceptionCheck()) { - throw std::runtime_error("Exception occurred while reading from InputStream"); + size_t read_fully(uint8_t *out, size_t len) { + if (!buffer.has_value() || env->GetArrayLength(buffer->get()) < len) { + buffer.emplace(env, env->NewByteArray(std::max(len, 512))); } - if (bytes_read > 0) { - env->GetByteArrayRegion(byte_array.get(), 0, bytes_read, reinterpret_cast(buffer)); - } - - return bytes_read; - } + size_t remaining = len; + while (remaining > 0) { + jint bytes_read = env->CallIntMethod(input_stream, + read_method, + buffer->get(), + static_cast(len - remaining), + static_cast(remaining)); + if (env->ExceptionCheck()) { + throw std::runtime_error("Exception occurred while reading from InputStream"); + } - 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"); + if (bytes_read <= 0) { + throw std::runtime_error("End of stream reached before reading requested number of bytes"); } - total_bytes_read += bytes_read; + + remaining -= bytes_read; } + + + env->GetByteArrayRegion(buffer->get(), 0, static_cast(len), reinterpret_cast(out)); + return len; } }; diff --git a/library/src/main/cpp/jni_utils.h b/library/src/main/cpp/jni_utils.h index ee990e5..44d599e 100644 --- a/library/src/main/cpp/jni_utils.h +++ b/library/src/main/cpp/jni_utils.h @@ -70,6 +70,13 @@ namespace jni_utils { } } + void reset(JNIType new_ref) { + if (ref_ != new_ref) { + env_->DeleteLocalRef(ref_); + } + ref_ = new_ref; + } + inline JNIType get() const { return ref_; } diff --git a/library/src/main/cpp/webp_utils.cpp b/library/src/main/cpp/webp_utils.cpp index 13b0a23..5eac299 100644 --- a/library/src/main/cpp/webp_utils.cpp +++ b/library/src/main/cpp/webp_utils.cpp @@ -137,28 +137,16 @@ extern "C" JNIEXPORT jbyteArray JNICALL Java_network_loki_messenger_libsession_1util_image_WebPUtils_encodeGifToWebP(JNIEnv *env, jobject thiz, - jobject input, + jbyteArray 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); + jni_utils::JavaByteArrayRef input_ref(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); + EasyGifReader decoder = EasyGifReader::openMemory(input_ref.bytes(), input_ref.size()); WebPPtr encoder(WebPAnimEncoderNew(target_width, target_height, nullptr), &WebPAnimEncoderDelete); if (!encoder) { @@ -175,7 +163,7 @@ Java_network_loki_messenger_libsession_1util_image_WebPUtils_encodeGifToWebP(JNI WebPPictureInit(pic.get()); pic->use_argb = 1; - for (auto frame = decoder.begin(); frame != decoder.end() && !is_timeout(); ++frame) { + for (auto frame = decoder.begin(); frame != decoder.end(); ++frame) { // Import the frame into a WebPPicture pic->width = decoder.width(); pic->height = decoder.height(); @@ -211,6 +199,12 @@ Java_network_loki_messenger_libsession_1util_image_WebPUtils_encodeGifToWebP(JNI WebPAnimEncoderGetError(encoder.get())); return nullptr; } + + if (std::chrono::high_resolution_clock::now() > deadline) { + env->ThrowNew(env->FindClass("java/util/concurrent/TimeoutException"), + "GIF re-encoding timed out"); + return nullptr; + } } WebPData out; diff --git a/library/src/main/java/network/loki/messenger/libsession_util/image/GifUtils.kt b/library/src/main/java/network/loki/messenger/libsession_util/image/GifUtils.kt index 7f96de9..da17fa0 100644 --- a/library/src/main/java/network/loki/messenger/libsession_util/image/GifUtils.kt +++ b/library/src/main/java/network/loki/messenger/libsession_util/image/GifUtils.kt @@ -15,12 +15,24 @@ object GifUtils : LibSessionUtilCApi() { * @return A byte array containing the re-encoded GIF data. */ external fun reencodeGif( - input: InputStream, + input: ByteArray, timeoutMills: Long, targetWidth: Int, targetHeight: Int ): ByteArray + fun isAnimatedGif(input: InputStream): Boolean { + return runCatching { + isAnimatedGifForStream(input) + }.getOrNull() == true + } + + fun isAnimatedGif(input: ByteArray): Boolean { + return runCatching { + isAnimatedGifForBytes(input) + }.getOrNull() == true + } + /** * Determines if the input stream contains an animated GIF. * @@ -28,7 +40,7 @@ object GifUtils : LibSessionUtilCApi() { * * @param input The input stream of the GIF to be checked. The caller is responsible for closing the stream. */ - external fun isAnimatedGif( - input: InputStream - ): Boolean + private external fun isAnimatedGifForStream(input: InputStream): Boolean + + private external fun isAnimatedGifForBytes(input: ByteArray): Boolean } \ 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 fcf338b..9698bd6 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 @@ -23,7 +23,7 @@ object WebPUtils : LibSessionUtilCApi() { ): ByteArray external fun encodeGifToWebP( - input: InputStream, + input: ByteArray, timeoutMills: Long, targetWidth: Int, targetHeight: Int,