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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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())
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
156 changes: 119 additions & 37 deletions library/src/main/cpp/gif_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cgif.h>
#include <libyuv.h>
#include <android/log.h>
#include <gif_lib.h>

#include <EasyGifReader.h>

Expand All @@ -18,17 +19,14 @@ using GifPtr = std::unique_ptr<T, void (*)(T *)>;
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<jbyteArray>(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<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
return size;
}, &input_stream);
EasyGifReader decoder = EasyGifReader::openMemory(input_ref.bytes(), input_ref.size());

std::vector<uint8_t> output_buffer;

Expand Down Expand Up @@ -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<uint8_t> decode_argb_buffer, encode_argb_buffer, encode_rgba_buffer;
if (needRescale) {
Expand All @@ -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<uint8_t*>(frame->pixels()),
Expand Down Expand Up @@ -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
Expand All @@ -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<GifByteType> 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<jboolean>(env, [=]() {
JniInputStream input_stream(env, input);
try {

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);

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<GifFileType> gif_file(DGifOpen(
&input_stream,
[](GifFileType *f, GifByteType *out_data, int size) -> int {
return reinterpret_cast<JniInputStream *>(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<jboolean>(env, [=]() {
jni_utils::JavaByteArrayRef input_ref(env, input);
auto input_data = input_ref.get();

int error = D_GIF_SUCCEEDED;
GifPtr<GifFileType> gif_file(DGifOpen(
&input_data,
[](GifFileType *f, GifByteType *out_data, int size) -> int {
auto input = reinterpret_cast<std::span<unsigned char> *>(f->UserData);
auto copy_count = std::min<size_t>(input->size(), static_cast<size_t>(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());
});

}
44 changes: 25 additions & 19 deletions library/src/main/cpp/jni_input_stream.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,44 @@ class JniInputStream {
JNIEnv *env;
jobject input_stream;
jmethodID read_method;
std::optional<jni_utils::JavaLocalRef<jbyteArray>> buffer;

public:
JniInputStream(JNIEnv *env, jobject input_stream)
: env(env), input_stream(input_stream) {
jni_utils::JavaLocalRef<jclass> 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<jbyteArray> byte_array(env, env->NewByteArray(static_cast<jsize>(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<jsize>(len, 512)));
}

if (bytes_read > 0) {
env->GetByteArrayRegion(byte_array.get(), 0, bytes_read, reinterpret_cast<jbyte *>(buffer));
}

return bytes_read;
}
size_t remaining = len;
while (remaining > 0) {
jint bytes_read = env->CallIntMethod(input_stream,
read_method,
buffer->get(),
static_cast<jint>(len - remaining),
static_cast<jint>(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<jint>(len), reinterpret_cast<jbyte *>(out));
return len;
}
};

Expand Down
7 changes: 7 additions & 0 deletions library/src/main/cpp/jni_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_;
}
Expand Down
26 changes: 10 additions & 16 deletions library/src/main/cpp/webp_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<jbyteArray>(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<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
return size;
}, &input_stream);
EasyGifReader decoder = EasyGifReader::openMemory(input_ref.bytes(), input_ref.size());

WebPPtr<WebPAnimEncoder> encoder(WebPAnimEncoderNew(target_width, target_height, nullptr), &WebPAnimEncoderDelete);
if (!encoder) {
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
Loading