Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 6 additions & 5 deletions library/src/main/cpp/gif_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<JniInputStream*>(ctx)->read(reinterpret_cast<uint8_t *>(out_buffer), size);
reinterpret_cast<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
return size;
}, &input_stream);

std::vector<uint8_t> output_buffer;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<JniInputStream *>(ctx)->read(
reinterpret_cast<uint8_t *>(out_buffer), size);
reinterpret_cast<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(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;
Expand Down
11 changes: 11 additions & 0 deletions library/src/main/cpp/jni_input_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
106 changes: 106 additions & 0 deletions library/src/main/cpp/webp_utils.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <jni.h>
#include "jni_utils.h"
#include "jni_input_stream.h"

#include <chrono>

Expand All @@ -8,6 +9,8 @@
#include <webp/decode.h>
#include <webp/encode.h>

#include <EasyGifReader.h>


template<typename T>
using WebPPtr = std::unique_ptr<T, void (*)(T *)>;
Expand Down Expand Up @@ -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<jbyteArray>(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<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
return size;
}, &input_stream);

WebPPtr<WebPAnimEncoder> 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<uint8_t> decode_argb_buffer(decoder.width() * decoder.height() * 4);
std::vector<uint8_t> encode_argb_buffer;
int frame_delay_ms = 0;

WebPPtr<WebPPicture> 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();
});
}
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -18,4 +21,11 @@ object WebPUtils {
targetWidth: Int,
targetHeight: Int,
): ByteArray

external fun encodeGifToWebP(
input: InputStream,
timeoutMills: Long,
targetWidth: Int,
targetHeight: Int,
): ByteArray
}