diff --git a/library/build.gradle.kts b/library/build.gradle.kts index e708086..ead2f0e 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -40,6 +40,14 @@ android { "proguard-rules.pro" ) } + + debug { + packaging { + jniLibs { + keepDebugSymbols += "**/*.so" + } + } + } } compileOptions { 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 new file mode 100644 index 0000000..b2b4125 --- /dev/null +++ b/library/src/androidTest/kotlin/network/loki/messenger/libsession_util/image/GifUtilsTest.kt @@ -0,0 +1,62 @@ +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 GifUtilsTest { + @Test + fun testReencodeGif() { + + for (outputSize in listOf(200, 400, 600)) { + val output = InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.earth).use { input -> + GifUtils.reencodeGif( + input = input, + 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) + } + } + } + + @Test + fun testCheckAnimatedGifWorks() { + assertTrue( + InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.earth) + .use(GifUtils::isAnimatedGif) + ) + + assertFalse( + InstrumentationRegistry.getInstrumentation() + .targetContext + .applicationContext + .resources + .openRawResource(R.raw.sunflower_noanim) + .use(GifUtils::isAnimatedGif) + ) + } +} \ No newline at end of file diff --git a/library/src/androidTest/res/raw/earth.gif b/library/src/androidTest/res/raw/earth.gif new file mode 100644 index 0000000..9df6477 Binary files /dev/null and b/library/src/androidTest/res/raw/earth.gif differ diff --git a/library/src/androidTest/res/raw/sunflower_noanim.gif b/library/src/androidTest/res/raw/sunflower_noanim.gif new file mode 100644 index 0000000..c895d7e Binary files /dev/null and b/library/src/androidTest/res/raw/sunflower_noanim.gif differ diff --git a/library/src/main/cpp/CMakeLists.txt b/library/src/main/cpp/CMakeLists.txt index 8256a10..c91afef 100644 --- a/library/src/main/cpp/CMakeLists.txt +++ b/library/src/main/cpp/CMakeLists.txt @@ -15,8 +15,6 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_FLAGS -Wno-deprecated-declarations) -set(CMAKE_BUILD_TYPE Release) - # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. @@ -26,6 +24,55 @@ set(STATIC_BUNDLE ON) set(ENABLE_ONIONREQ OFF) add_subdirectory(../../../../libsession-util libsession) +include(FetchContent) + + +## Catering for libwebp +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(WEBP_BUILD_ANIM_UTILS ON CACHE BOOL "" FORCE) +set(WEBP_BUILD_LIBWEBPMUX ON CACHE BOOL "" FORCE) + +FetchContent_Declare( + webp + GIT_REPOSITORY https://chromium.googlesource.com/webm/libwebp + GIT_TAG v1.6.0 +) + +FetchContent_MakeAvailable(webp) + +## Catering for libyuv - to rescale images +FetchContent_Declare( + libyuv + GIT_REPOSITORY https://chromium.googlesource.com/libyuv/libyuv + GIT_TAG stable +) +FetchContent_MakeAvailable(libyuv) + +## cgif +FetchContent_Declare( + cgif + GIT_REPOSITORY https://github.com/dloebl/cgif.git + GIT_TAG 4ddc417 +) +FetchContent_MakeAvailable(cgif) + +## giflib (only to encode) +FetchContent_Declare( + giflib + GIT_REPOSITORY https://git.code.sf.net/p/giflib/code + GIT_TAG 5.2.2 +) +FetchContent_MakeAvailable(giflib) + +## EasyGifReader (on top of giflib for easier reading) +FetchContent_Declare( + easy_gif_reader + GIT_REPOSITORY https://github.com/Chlumsky/EasyGifReader + GIT_TAG eaaa794 +) +FetchContent_MakeAvailable(easy_gif_reader) + + set(SOURCES user_profile.cpp user_groups.cpp @@ -44,6 +91,23 @@ set(SOURCES ed25519.cpp curve25519.cpp hash.cpp + attachments.cpp + webp_utils.cpp + gif_utils.cpp + + ${cgif_SOURCE_DIR}/src/cgif.c + ${cgif_SOURCE_DIR}/src/cgif_rgb.c + ${cgif_SOURCE_DIR}/src/cgif_raw.c + + ${giflib_SOURCE_DIR}/gifalloc.c + ${giflib_SOURCE_DIR}/gif_err.c + ${giflib_SOURCE_DIR}/dgif_lib.c + ${giflib_SOURCE_DIR}/egif_lib.c + ${giflib_SOURCE_DIR}/gif_hash.c + ${giflib_SOURCE_DIR}/gif_font.c + ${giflib_SOURCE_DIR}/openbsd-reallocarray.c + + ${easy_gif_reader_SOURCE_DIR}/EasyGifReader.cpp ) add_library( # Sets the name of the library. @@ -53,6 +117,15 @@ add_library( # Sets the name of the library. # Provides a relative path to your source file(s). ${SOURCES}) +target_include_directories(session_util + PRIVATE + ${cgif_SOURCE_DIR}/inc + ${libyuv_SOURCE_DIR}/include + ${giflib_SOURCE_DIR} + ${easy_gif_reader_SOURCE_DIR} +) + + # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by # default, you only need to specify the name of the public NDK library @@ -78,4 +151,10 @@ target_link_libraries( # Specifies the target library. libsodium::sodium-internal # Links the target library to the log library # included in the NDK. - ${log-lib}) + ${log-lib} + webp + libwebpmux + webpdemux + webpdecoder + yuv +) \ No newline at end of file diff --git a/library/src/main/cpp/attachments.cpp b/library/src/main/cpp/attachments.cpp new file mode 100644 index 0000000..109c256 --- /dev/null +++ b/library/src/main/cpp/attachments.cpp @@ -0,0 +1,79 @@ +#include +#include +#include "jni_utils.h" + +using namespace session::attachment; +using namespace jni_utils; + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_encrypt_Attachments_encryptedSize(JNIEnv *env, + jobject thiz, + jlong plaintext_size) { + return encrypted_size(plaintext_size); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_encrypt_Attachments_decryptedMaxSize(JNIEnv *env, + jobject thiz, + jlong ciphertext_size) { + auto s = decrypted_max_size(ciphertext_size); + if (s.has_value()) { + return *s; + } else { + env->ThrowNew(env->FindClass("java/lang/IllegalArgumentException"), + "ciphertext_size too small to be a valid encrypted attachment"); + return 0; + } +} + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_encrypt_Attachments_encryptBytes(JNIEnv *env, + jobject thiz, + jbyteArray seed, + jbyteArray plaintext_in, + jint plaintext_in_offset, + jint plaintext_in_len, + jbyteArray cipher_out, + jint cipher_out_offset, + jint cipher_out_len, + jint domain) { + return run_catching_cxx_exception_or_throws(env, [=] { + auto key = encrypt( + JavaByteArrayRef(env, seed).get_as_bytes(), + JavaByteArrayRef(env, plaintext_in).get_as_bytes().subspan(plaintext_in_offset, plaintext_in_len), + static_cast(domain), + JavaByteArrayRef(env, cipher_out).get_as_bytes().subspan(cipher_out_offset, cipher_out_len) + ); + + + return util::bytes_from_span(env, std::span(reinterpret_cast(key.data()), key.size())); + }); +} + +extern "C" +JNIEXPORT jlong JNICALL +Java_network_loki_messenger_libsession_1util_encrypt_Attachments_decryptBytes(JNIEnv *env, + jobject thiz, + jbyteArray key, + jbyteArray cipher_in, + jint cipher_in_offset, + jint cipher_in_len, + jbyteArray plain_out, + jint plain_out_offset, + jint plain_out_len) { + return run_catching_cxx_exception_or_throws(env, [=] { + JavaByteArrayRef key_ref(env, key); + + return decrypt( + JavaByteArrayRef(env, cipher_in).get_as_bytes().subspan(cipher_in_offset, cipher_in_len), + std::span( + reinterpret_cast(key_ref.get().data()), + ENCRYPT_KEY_SIZE + ), + JavaByteArrayRef(env, plain_out).get_as_bytes().subspan(plain_out_offset, plain_out_len) + ); + }); +} \ No newline at end of file diff --git a/library/src/main/cpp/gif_utils.cpp b/library/src/main/cpp/gif_utils.cpp new file mode 100644 index 0000000..ba6a174 --- /dev/null +++ b/library/src/main/cpp/gif_utils.cpp @@ -0,0 +1,157 @@ +#include +#include +#include + +#include "jni_utils.h" +#include "jni_input_stream.h" + +#include +#include +#include + +#include + +template +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, + 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); + + EasyGifReader decoder = EasyGifReader::openCustom([](void *out_buffer, size_t size, void *ctx) { + return reinterpret_cast(ctx)->read(reinterpret_cast(out_buffer), size); + }, &input_stream); + + std::vector output_buffer; + + CGIFrgb_Config encode_config = { + .pWriteFn = [](void* pContext, const uint8_t* pData, const size_t numBytes) -> int { + auto* output_buffer = static_cast*>(pContext); + output_buffer->insert(output_buffer->end(), pData, pData + numBytes); + return 0; // success + }, + .pContext = &output_buffer, + .path = nullptr, + .attrFlags = 0, + .genFlags = 0, + .numLoops = static_cast(decoder.repeatsInfinitely() ? 0 : decoder.repeatCount()), + .width = static_cast(target_width), + .height = static_cast(target_height), + }; + + GifPtr encoder(cgif_rgb_newgif(&encode_config), [](CGIFrgb* ptr) { cgif_rgb_close(ptr); }); + if (!encoder) { + throw std::runtime_error("Failed to create GIF encoder"); + } + + const auto src_width = decoder.width(); + const auto src_height = decoder.height(); + + 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) { + decode_argb_buffer.resize(src_width * src_height * 4); + encode_argb_buffer.resize(target_width * target_height * 4); + encode_rgba_buffer.resize(target_width * target_height * 4); + } + + for (auto frame = decoder.begin(); frame != decoder.end() && !is_timeout(); ++frame) { + // Here we would add the frame to the encoder + CGIFrgb_FrameConfig config = { + .pImageData = const_cast(frame->pixels()), + .fmtChan = CGIF_CHAN_FMT_RGBA, + .attrFlags = 0, + .genFlags = 0, + .delay = static_cast(frame.rawDuration().centiseconds), // in 0.01s + }; + + if (needRescale) { + // First we need to convert our frame data from RGBA to ARGB + libyuv::RGBAToARGB( + frame->pixels(), src_width * 4, + decode_argb_buffer.data(), src_width * 4, + src_width, src_height + ); + + // Scale it to target size + libyuv::ARGBScale( + decode_argb_buffer.data(), src_width * 4, + src_width, src_height, + encode_argb_buffer.data(), target_width * 4, + target_width, target_height, + libyuv::kFilterBox + ); + + // Convert the scaled ARGB32 back to RGB24 for encoding + libyuv::ARGBToRGBA( + encode_argb_buffer.data(), target_width * 4, + encode_rgba_buffer.data(), target_width * 4, + target_width, target_height + ); + + // Override the image data pointer to the rescaled data + 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"); + } + } + + // Close the encoder to finalize the GIF + encoder.reset(); + + jni_utils::JavaByteArrayRef output_array(env, env->NewByteArray(output_buffer.size())); + std::memcpy(output_array.bytes(), output_buffer.data(), output_buffer.size()); + return output_array.java_array(); + }); +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_network_loki_messenger_libsession_1util_image_GifUtils_isAnimatedGif(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) { + return reinterpret_cast(ctx)->read( + reinterpret_cast(out_buffer), size); + }, &input_stream); + + return decoder.frameCount() > 1; + } catch (...) { + // 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; + } + }); +} \ 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 new file mode 100644 index 0000000..ec4a09f --- /dev/null +++ b/library/src/main/cpp/jni_input_stream.h @@ -0,0 +1,37 @@ +#ifndef LIBSESSION_UTIL_ANDROID_JNI_INPUT_STREAM_H +#define LIBSESSION_UTIL_ANDROID_JNI_INPUT_STREAM_H + +#include + +#include "jni_utils.h" + +class JniInputStream { +private: + JNIEnv *env; + jobject input_stream; + jmethodID read_method; + +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"); + } + + 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()); + + if (env->ExceptionCheck()) { + throw std::runtime_error("Exception occurred while reading from InputStream"); + } + + if (bytes_read > 0) { + env->GetByteArrayRegion(byte_array.get(), 0, bytes_read, reinterpret_cast(buffer)); + } + + return bytes_read; + } +}; + +#endif //LIBSESSION_UTIL_ANDROID_JNI_INPUT_STREAM_H diff --git a/library/src/main/cpp/jni_utils.h b/library/src/main/cpp/jni_utils.h index 5aca809..ee990e5 100644 --- a/library/src/main/cpp/jni_utils.h +++ b/library/src/main/cpp/jni_utils.h @@ -199,11 +199,31 @@ namespace jni_utils { env->ReleaseByteArrayElements(byte_array, reinterpret_cast(data.data()), 0); } + jbyteArray java_array() const { + return byte_array; + } + + const unsigned char * bytes() const { + return data.data(); + } + + unsigned char *bytes() { + return data.data(); + } + + size_t size() const { + return data.size(); + } + // Get the data as a span. Only valid during the lifetime of this object. std::span get() const { return data; } + std::span get_as_bytes() const { + return std::span(reinterpret_cast(data.data()), data.size()); + } + std::vector copy() const { return std::vector(data.begin(), data.end()); } diff --git a/library/src/main/cpp/webp_utils.cpp b/library/src/main/cpp/webp_utils.cpp new file mode 100644 index 0000000..a357730 --- /dev/null +++ b/library/src/main/cpp/webp_utils.cpp @@ -0,0 +1,125 @@ +#include +#include "jni_utils.h" + +#include + +#include +#include +#include +#include + + +template +using WebPPtr = std::unique_ptr; + +extern "C" +JNIEXPORT jbyteArray JNICALL +Java_network_loki_messenger_libsession_1util_image_WebPUtils_reencodeWebPAnimation(JNIEnv *env, + jobject thiz, + jbyteArray input, + jlong timeout_ms, + jint target_width, + jint target_height) { + jni_utils::JavaByteArrayRef input_ref(env, input); + WebPData input_data = { + .bytes = input_ref.bytes(), + .size = input_ref.size(), + }; + + WebPAnimDecoderOptions opts; + WebPAnimDecoderOptionsInit(&opts); + + opts.color_mode = MODE_RGBA; + opts.use_threads = 1; + + WebPPtr decoder(WebPAnimDecoderNew(&input_data, &opts), &WebPAnimDecoderDelete); + + if (!decoder) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to create animation decoder"); + return nullptr; + } + + WebPAnimInfo info; + if (!WebPAnimDecoderGetInfo(decoder.get(), &info)) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to get animation info"); + return nullptr; + } + + 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; + } + + + int ts_mills = 0; + uint8_t *frame_data = nullptr; + const auto deadline = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(timeout_ms); + + while (WebPAnimDecoderGetNext(decoder.get(), &frame_data, &ts_mills)) { + WebPPicture pic; + WebPPictureInit(&pic); + + // First, import the frame into a picture + pic.width = info.canvas_width; + pic.height = info.canvas_height; + pic.use_argb = 0; + + if (!WebPPictureImportRGBA(&pic, frame_data, info.canvas_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 picture + if (target_width != info.canvas_width || target_height != info.canvas_height) { + // Re-scale the picture to the target size + if (!WebPPictureRescale(&pic, target_width, target_height)) { + WebPPictureFree(&pic); + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to rescale picture"); + return nullptr; + } + } + + // Now add the picture to the encoder + WebPConfig encode_config; + WebPConfigInit(&encode_config); + encode_config.quality = 95.f; + + auto encode_succeeded = WebPAnimEncoderAdd(encoder.get(), &pic, ts_mills, &encode_config); + + // Free the picture as sonn as we're done with it + WebPPictureFree(&pic); + + if (!encode_succeeded) { + env->ThrowNew(env->FindClass("java/lang/RuntimeException"), + "Failed to add frame to encoder"); + return nullptr; + } + + if (std::chrono::high_resolution_clock::now() > deadline) { + env->ThrowNew(env->FindClass("java/util/concurrent/TimeoutException"), + "Re-encoding animation timed out"); + 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(); +} diff --git a/library/src/main/java/network/loki/messenger/libsession_util/encrypt/Attachments.kt b/library/src/main/java/network/loki/messenger/libsession_util/encrypt/Attachments.kt new file mode 100644 index 0000000..16d7990 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/encrypt/Attachments.kt @@ -0,0 +1,107 @@ +package network.loki.messenger.libsession_util.encrypt + +import network.loki.messenger.libsession_util.LibSessionUtilCApi + +/** + * Utilities for encrypting and decrypting attachments and profile pictures uploaded to the FileServer. + */ +object Attachments : LibSessionUtilCApi() { + enum class Domain(val nativeValue: Int) { + Attachment(0), + ProfilePic(1), + } + + /** + * Returns the size of the encrypted data given the size of the plaintext. + */ + external fun encryptedSize(plaintextSize: Long): Long + + /** + * Returns the size of the encrypted data given the size of the plaintext, or null if the size + * given is smaller than the minimum encryption overhead. + */ + fun decryptedMaxSizeOrNull(ciphertextSize: Long): Long? = runCatching { + decryptedMaxSize(ciphertextSize) + }.getOrNull() + + + private external fun decryptedMaxSize(ciphertextSize: Long): Long + + private external fun encryptBytes( + seed: ByteArray, + plaintextIn: ByteArray, + plaintextInOffset: Int, + plaintextInLen: Int, + cipherOut: ByteArray, + cipherOutOffset: Int, + cipherOutLen: Int, + domain: Int + ): ByteArray + + /** + * Encrypts the given plaintext using a key derived from the given seed and the given domain. + * + * The output needs to be allocated by the caller, its size can be determined by calling + * [encryptedSize] with the size of the plaintext. + * + * @return The key used to encrypt the data. + */ + fun encryptBytes( + seed: ByteArray, + plaintextIn: ByteArray, + cipherOut: ByteArray, + domain: Domain + ): ByteArray = encryptBytes( + seed = seed, + plaintextIn = plaintextIn, + plaintextInLen = plaintextIn.size, + plaintextInOffset = 0, + cipherOut = cipherOut, + cipherOutOffset = 0, + cipherOutLen = cipherOut.size, + domain = domain.nativeValue + ) + + /** + * Decrypts the given ciphertext using the given key. + * + * The output needs to be allocated by the caller, its size can be determined by calling + * [decryptedMaxSizeOrNull] with the size of the ciphertext. + * + * Will throw if there's any error during decryption. + * + * @return The size of the decrypted data. + */ + external fun decryptBytes( + key: ByteArray, + cipherIn: ByteArray, + cipherInOffset: Int, + cipherInLen: Int, + plainOut: ByteArray, + plainOutOffset: Int, + plainOutLen: Int, + ): Long + + /** + * Decrypts the given ciphertext using the given key. + * + * The output needs to be allocated by the caller, its size can be determined by calling + * [decryptedMaxSizeOrNull] with the size of the ciphertext. + * Will throw if there's any error during decryption. + * + * @return The size of the decrypted data. + */ + fun decryptBytes( + key: ByteArray, + cipherIn: ByteArray, + plainOut: ByteArray + ): Long = decryptBytes( + key = key, + cipherIn = cipherIn, + cipherInOffset = 0, + cipherInLen = cipherIn.size, + plainOut = plainOut, + plainOutOffset = 0, + plainOutLen = plainOut.size + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000..7f96de9 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/image/GifUtils.kt @@ -0,0 +1,34 @@ +package network.loki.messenger.libsession_util.image + +import network.loki.messenger.libsession_util.LibSessionUtilCApi +import java.io.InputStream + +object GifUtils : LibSessionUtilCApi() { + /** + * Re-encodes an input GIF to a target width and height. + * + * @param input The input stream of the GIF to be re-encoded. The caller is responsible for closing the stream. + * @param timeoutMills The maximum time in milliseconds to allow for the re-encoding process. + * If the process exceeds this time, a [java.util.concurrent.TimeoutException] will be thrown. + * @param targetWidth The desired width of the output GIF. + * @param targetHeight The desired height of the output GIF. + * @return A byte array containing the re-encoded GIF data. + */ + external fun reencodeGif( + input: InputStream, + timeoutMills: Long, + targetWidth: Int, + targetHeight: Int + ): ByteArray + + /** + * Determines if the input stream contains an animated GIF. + * + * An animated GIF is defined as a GIF file with more than one frame. + * + * @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 +} \ 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 new file mode 100644 index 0000000..07976b2 --- /dev/null +++ b/library/src/main/java/network/loki/messenger/libsession_util/image/WebPUtils.kt @@ -0,0 +1,21 @@ +package network.loki.messenger.libsession_util.image + +object WebPUtils { + /** + * Re-encode the webP animation, resizing each frame to scale to the target width and height. + * This can serve two purposes: + * 1. Getting rid of any extra metadata that might be present in the original file. + * 2. Reducing the dimensions of the animation to fit within specified bounds. + * + * If you only want to remove metadata, you can give the original size as targetWidth and targetHeight, + * this function will then not try to resize the image. + * + * @throws java.util.concurrent.TimeoutException if the operation takes longer than timeoutMills milliseconds. + */ + external fun reencodeWebPAnimation( + input: ByteArray, + timeoutMills: Long, + targetWidth: Int, + targetHeight: Int, + ): ByteArray +} \ No newline at end of file diff --git a/libsession-util b/libsession-util index c73fd92..1fdd145 160000 --- a/libsession-util +++ b/libsession-util @@ -1 +1 @@ -Subproject commit c73fd92eb71e8108cc435fc640fbcb8faceacbb0 +Subproject commit 1fdd145076cb932a78fdde6e37753bacc2848fff