Skip to content

Commit 5a1dd53

Browse files
Add gif -> webp encoder (#27)
1 parent bf8871b commit 5a1dd53

File tree

6 files changed

+203
-6
lines changed

6 files changed

+203
-6
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package network.loki.messenger.libsession_util.image
2+
3+
import android.graphics.ImageDecoder
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
import androidx.test.platform.app.InstrumentationRegistry
6+
import org.junit.Assert.assertEquals
7+
import org.junit.Assert.assertFalse
8+
import org.junit.Assert.assertTrue
9+
import org.junit.Test
10+
import org.junit.runner.RunWith
11+
import org.sessionfoundation.libsession_util.test.R
12+
13+
@RunWith(AndroidJUnit4::class)
14+
class WebUtilsTest {
15+
@Test
16+
fun testReencodeWebP() {
17+
18+
for (outputSize in listOf(200, 400, 600)) {
19+
val output = InstrumentationRegistry.getInstrumentation()
20+
.targetContext
21+
.applicationContext
22+
.resources
23+
.openRawResource(R.raw.webp_animated).use { input ->
24+
WebPUtils.reencodeWebPAnimation(
25+
input = input.readAllBytes(),
26+
timeoutMills = 100_000L,
27+
targetWidth = outputSize,
28+
targetHeight = outputSize
29+
)
30+
}
31+
32+
ImageDecoder.decodeDrawable(
33+
ImageDecoder.createSource(output)
34+
) { decoder, info, source ->
35+
assertEquals(outputSize, info.size.width)
36+
assertEquals(outputSize, info.size.height)
37+
assertTrue(info.isAnimated)
38+
assertEquals("image/webp", info.mimeType)
39+
}
40+
}
41+
}
42+
43+
@Test
44+
fun testEncodeGIFToWebP() {
45+
for (outputSize in listOf(200, 400, 600)) {
46+
val output = InstrumentationRegistry.getInstrumentation()
47+
.targetContext
48+
.applicationContext
49+
.resources
50+
.openRawResource(R.raw.earth).use { input ->
51+
WebPUtils.encodeGifToWebP(
52+
input = input,
53+
timeoutMills = 100_000L,
54+
targetWidth = outputSize,
55+
targetHeight = outputSize
56+
)
57+
}
58+
59+
ImageDecoder.decodeDrawable(
60+
ImageDecoder.createSource(output)
61+
) { decoder, info, source ->
62+
assertEquals("image/webp", info.mimeType)
63+
assertEquals(outputSize, info.size.width)
64+
assertEquals(outputSize, info.size.height)
65+
assertTrue(info.isAnimated)
66+
}
67+
}
68+
}
69+
}
127 KB
Loading

library/src/main/cpp/gif_utils.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv *
2626
JniInputStream input_stream(env, input);
2727

2828
EasyGifReader decoder = EasyGifReader::openCustom([](void *out_buffer, size_t size, void *ctx) {
29-
return reinterpret_cast<JniInputStream*>(ctx)->read(reinterpret_cast<uint8_t *>(out_buffer), size);
29+
reinterpret_cast<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
30+
return size;
3031
}, &input_stream);
3132

3233
std::vector<uint8_t> output_buffer;
@@ -100,7 +101,7 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_reencodeGif(JNIEnv *
100101
libyuv::kFilterBox
101102
);
102103

103-
// Convert the scaled ARGB32 back to RGB24 for encoding
104+
// Convert the scaled ARGB32 back to RGBA for encoding
104105
libyuv::ARGBToRGBA(
105106
encode_argb_buffer.data(), target_width * 4,
106107
encode_rgba_buffer.data(), target_width * 4,
@@ -139,12 +140,12 @@ Java_network_loki_messenger_libsession_1util_image_GifUtils_isAnimatedGif(JNIEnv
139140

140141
EasyGifReader decoder = EasyGifReader::openCustom(
141142
[](void *out_buffer, size_t size, void *ctx) {
142-
return reinterpret_cast<JniInputStream *>(ctx)->read(
143-
reinterpret_cast<uint8_t *>(out_buffer), size);
143+
reinterpret_cast<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
144+
return size;
144145
}, &input_stream);
145146

146147
return decoder.frameCount() > 1;
147-
} catch (...) {
148+
} catch (const EasyGifReader::Error &e) {
148149
// Is there's a java exception pending?
149150
if (env->ExceptionCheck()) {
150151
return false;

library/src/main/cpp/jni_input_stream.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ class JniInputStream {
3232

3333
return bytes_read;
3434
}
35+
36+
void read_fully(uint8_t *buffer, size_t size) {
37+
size_t total_bytes_read = 0;
38+
while (total_bytes_read < size) {
39+
size_t bytes_read = read(buffer + total_bytes_read, size - total_bytes_read);
40+
if (bytes_read == 0) {
41+
throw std::runtime_error("EOF reached");
42+
}
43+
total_bytes_read += bytes_read;
44+
}
45+
}
3546
};
3647

3748
#endif //LIBSESSION_UTIL_ANDROID_JNI_INPUT_STREAM_H

library/src/main/cpp/webp_utils.cpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <jni.h>
22
#include "jni_utils.h"
3+
#include "jni_input_stream.h"
34

45
#include <chrono>
56

@@ -8,6 +9,8 @@
89
#include <webp/decode.h>
910
#include <webp/encode.h>
1011

12+
#include <EasyGifReader.h>
13+
1114

1215
template<typename T>
1316
using WebPPtr = std::unique_ptr<T, void (*)(T *)>;
@@ -123,3 +126,106 @@ Java_network_loki_messenger_libsession_1util_image_WebPUtils_reencodeWebPAnimati
123126

124127
return out_ref.java_array();
125128
}
129+
130+
131+
static void destroyWebPPicture(WebPPicture* pic) {
132+
WebPPictureFree(pic);
133+
delete pic;
134+
}
135+
136+
extern "C"
137+
JNIEXPORT jbyteArray JNICALL
138+
Java_network_loki_messenger_libsession_1util_image_WebPUtils_encodeGifToWebP(JNIEnv *env,
139+
jobject thiz,
140+
jobject input,
141+
jlong timeout_mills,
142+
jint target_width,
143+
jint target_height) {
144+
return jni_utils::run_catching_cxx_exception_or_throws<jbyteArray>(env, [=]() -> jbyteArray {
145+
JniInputStream input_stream(env, input);
146+
147+
const auto deadline = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(timeout_mills);
148+
149+
auto is_timeout = [&]() {
150+
if (std::chrono::high_resolution_clock::now() > deadline) {
151+
env->ThrowNew(env->FindClass("java/util/concurrent/TimeoutException"),
152+
"GIF re-encoding timed out");
153+
return true;
154+
}
155+
return false;
156+
};
157+
158+
EasyGifReader decoder = EasyGifReader::openCustom([](void *out_buffer, size_t size, void *ctx) {
159+
reinterpret_cast<JniInputStream*>(ctx)->read_fully(reinterpret_cast<uint8_t *>(out_buffer), size);
160+
return size;
161+
}, &input_stream);
162+
163+
WebPPtr<WebPAnimEncoder> encoder(WebPAnimEncoderNew(target_width, target_height, nullptr), &WebPAnimEncoderDelete);
164+
if (!encoder) {
165+
env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
166+
"Failed to create animation encoder");
167+
return 0;
168+
}
169+
170+
std::vector<uint8_t> decode_argb_buffer(decoder.width() * decoder.height() * 4);
171+
std::vector<uint8_t> encode_argb_buffer;
172+
int frame_delay_ms = 0;
173+
174+
WebPPtr<WebPPicture> pic(new WebPPicture, &destroyWebPPicture);
175+
WebPPictureInit(pic.get());
176+
pic->use_argb = 1;
177+
178+
for (auto frame = decoder.begin(); frame != decoder.end() && !is_timeout(); ++frame) {
179+
// Import the frame into a WebPPicture
180+
pic->width = decoder.width();
181+
pic->height = decoder.height();
182+
pic->argb_stride = decoder.width();
183+
184+
if (!WebPPictureImportRGBA(pic.get(), frame->pixels(), decoder.width() * 4)) {
185+
env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
186+
"Failed to import frame into picture");
187+
return nullptr;
188+
}
189+
190+
// If the target size is different, rescale the frame
191+
if (target_width != decoder.width() || target_height != decoder.height()) {
192+
if (!WebPPictureRescale(pic.get(), target_width, target_height)) {
193+
env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
194+
"Failed to rescale picture");
195+
return nullptr;
196+
}
197+
}
198+
199+
// Now we have a WebPPicture ready to be added to the encoder
200+
WebPConfig encode_config;
201+
WebPConfigInit(&encode_config);
202+
encode_config.quality = 95.f;
203+
204+
frame_delay_ms += frame.duration().milliseconds();
205+
206+
auto encode_succeeded = WebPAnimEncoderAdd(encoder.get(), pic.get(),
207+
frame_delay_ms, &encode_config);
208+
209+
if (!encode_succeeded) {
210+
env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
211+
WebPAnimEncoderGetError(encoder.get()));
212+
return nullptr;
213+
}
214+
}
215+
216+
WebPData out;
217+
WebPDataInit(&out);
218+
219+
if (!WebPAnimEncoderAssemble(encoder.get(), &out)) {
220+
env->ThrowNew(env->FindClass("java/lang/RuntimeException"),
221+
"Failed to assemble animation");
222+
return nullptr;
223+
}
224+
225+
jni_utils::JavaByteArrayRef out_ref(env, env->NewByteArray(out.size));
226+
std::memcpy(out_ref.bytes(), out.bytes, out.size);
227+
WebPDataClear(&out);
228+
229+
return out_ref.java_array();
230+
});
231+
}

library/src/main/java/network/loki/messenger/libsession_util/image/WebPUtils.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package network.loki.messenger.libsession_util.image
22

3-
object WebPUtils {
3+
import network.loki.messenger.libsession_util.LibSessionUtilCApi
4+
import java.io.InputStream
5+
6+
object WebPUtils : LibSessionUtilCApi() {
47
/**
58
* Re-encode the webP animation, resizing each frame to scale to the target width and height.
69
* This can serve two purposes:
@@ -18,4 +21,11 @@ object WebPUtils {
1821
targetWidth: Int,
1922
targetHeight: Int,
2023
): ByteArray
24+
25+
external fun encodeGifToWebP(
26+
input: InputStream,
27+
timeoutMills: Long,
28+
targetWidth: Int,
29+
targetHeight: Int,
30+
): ByteArray
2131
}

0 commit comments

Comments
 (0)