Skip to content

Commit

Permalink
feat: allow to decode only one frame and don't alloc AnimationDrawabl…
Browse files Browse the repository at this point in the history
…e when no animation.
  • Loading branch information
oupson committed Nov 29, 2024
1 parent 2bc2e4b commit e71fceb
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 39 deletions.
9 changes: 5 additions & 4 deletions app/src/main/java/fr/oupson/jxlviewer/ViewerActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fr.oupson.jxlviewer

import android.graphics.Bitmap
import android.graphics.drawable.AnimationDrawable
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.os.ParcelFileDescriptor
Expand Down Expand Up @@ -34,10 +35,10 @@ class ViewerActivity : ComponentActivity() {
} else {
Bitmap.Config.ARGB_8888
}
decodeMultipleFrames = true
}



override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

Expand Down Expand Up @@ -72,7 +73,7 @@ class ViewerActivity : ComponentActivity() {

withContext(Dispatchers.Main) {
binding.test.setImageDrawable(image)
image?.start()
(image as? AnimationDrawable)?.start()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load image", e)
Expand All @@ -88,11 +89,11 @@ class ViewerActivity : ComponentActivity() {
}
}

private fun loadImage(input: InputStream): AnimationDrawable? = input.use {
private fun loadImage(input: InputStream): Drawable? = input.use {
JxlDecoder.loadJxl(it, decoderConfig)
}

private fun loadImage(fd: ParcelFileDescriptor): AnimationDrawable? = fd.use {
private fun loadImage(fd: ParcelFileDescriptor): Drawable? = fd.use {
JxlDecoder.loadJxl(it, decoderConfig)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import android.graphics.Bitmap;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

Expand All @@ -21,6 +22,7 @@
import java.nio.file.Path;
import java.util.Objects;

import fr.oupson.libjxl.exceptions.ConfigException;
import fr.oupson.libjxl.exceptions.DecodeError;

public class JxlDecodeAndroidUnitTest {
Expand All @@ -30,12 +32,10 @@ public void decode_LogoShouldNotFail() throws IOException {
InputStream input = context.getResources().getAssets().open("logo.jxl");

try {
AnimationDrawable result = JxlDecoder.loadJxl(input, null);
BitmapDrawable result = (BitmapDrawable) JxlDecoder.loadJxl(input, null);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 1, result.getNumberOfFrames());
BitmapDrawable frame = (BitmapDrawable) result.getFrame(0);
Assert.assertEquals("Invalid image width", 1000, frame.getBitmap().getWidth());
Assert.assertEquals("Invalid image height", 1000, frame.getBitmap().getHeight());
Assert.assertEquals("Invalid image width", 1000, result.getBitmap().getWidth());
Assert.assertEquals("Invalid image height", 1000, result.getBitmap().getHeight());
} catch (Exception e) {
Assert.fail("Failed to read image : " + e.getMessage());
}
Expand All @@ -47,12 +47,10 @@ public void decode_DidiShouldNotFail() throws IOException {
InputStream input = context.getResources().getAssets().open("didi.jxl");

try {
AnimationDrawable result = JxlDecoder.loadJxl(input, null);
BitmapDrawable result = (BitmapDrawable) JxlDecoder.loadJxl(input, null);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 1, result.getNumberOfFrames());
BitmapDrawable frame = (BitmapDrawable) result.getFrame(0);
Assert.assertEquals("Invalid image width", 2048, frame.getBitmap().getWidth());
Assert.assertEquals("Invalid image height", 1536, frame.getBitmap().getHeight());
Assert.assertEquals("Invalid image width", 2048, result.getBitmap().getWidth());
Assert.assertEquals("Invalid image height", 1536, result.getBitmap().getHeight());
} catch (Exception e) {
Assert.fail("Failed to read image : " + e.getMessage());
}
Expand All @@ -64,7 +62,7 @@ public void decode_FerrisShouldNotFail() throws IOException {
InputStream input = context.getResources().getAssets().open("ferris.jxl");

try {
AnimationDrawable result = JxlDecoder.loadJxl(input, null);
AnimationDrawable result = (AnimationDrawable) JxlDecoder.loadJxl(input, null);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 27, result.getNumberOfFrames());

Expand All @@ -78,6 +76,44 @@ public void decode_FerrisShouldNotFail() throws IOException {
}
}

@Test
public void decode_FerrisWithDecodeMultipleFramesFalseShouldDecodeOneFrame() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
InputStream input = context.getResources().getAssets().open("ferris.jxl");

try (JxlDecoder.Options options = new JxlDecoder.Options().setDecodeMultipleFrames(false)) {
Drawable result = JxlDecoder.loadJxl(input, options);
Assert.assertNotNull(result);
Assert.assertNotEquals(AnimationDrawable.class, result.getClass());
} catch (Exception e) {
Assert.fail("Failed to read image : " + e.getMessage());
}
}

@Test
public void decode_FerrisWithDecodeMultipleFramesTrueShouldDecodeMultipleFrames() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
InputStream input = context.getResources().getAssets().open("ferris.jxl");

try (JxlDecoder.Options options = new JxlDecoder.Options().setDecodeMultipleFrames(true)) {
Drawable result = JxlDecoder.loadJxl(input, options);
Assert.assertNotNull(result);
Assert.assertEquals(AnimationDrawable.class, result.getClass());

AnimationDrawable animation = (AnimationDrawable) result;

Assert.assertEquals("Invalid number of frames", 27, animation.getNumberOfFrames());

for (int i = 0; i < 27; i++) {
BitmapDrawable frame = (BitmapDrawable) animation.getFrame(i);
Assert.assertEquals("Invalid frame width", 378, frame.getBitmap().getWidth());
Assert.assertEquals("Invalid frame height", 300, frame.getBitmap().getHeight());
}
} catch (Exception e) {
Assert.fail("Failed to read image : " + e.getMessage());
}
}

@Test
public void decode_FerrisWithParcelFileDescriptorShouldNotFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
Expand All @@ -91,7 +127,7 @@ public void decode_FerrisWithParcelFileDescriptorShouldNotFail() throws IOExcept
Uri androidUri = Uri.fromFile(testFile.toFile());

try (ParcelFileDescriptor input = context.getContentResolver().openFileDescriptor(androidUri, "r")) {
AnimationDrawable result = JxlDecoder.loadJxl(Objects.requireNonNull(input), null);
AnimationDrawable result = (AnimationDrawable) JxlDecoder.loadJxl(Objects.requireNonNull(input), null);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 27, result.getNumberOfFrames());

Expand All @@ -116,7 +152,7 @@ public void decode_WithoutEnoughInputShouldFail() throws IOException {
input.close();

DecodeError error = Assert.assertThrows(DecodeError.class, () -> {
AnimationDrawable result = JxlDecoder.loadJxl(new ByteArrayInputStream(content), null);
Drawable result = JxlDecoder.loadJxl(new ByteArrayInputStream(content), null);
});

Assert.assertEquals(DecodeError.DecodeErrorType.NeedMoreInputError, error.getErrorType());
Expand All @@ -127,7 +163,7 @@ public void decode_PngShouldFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
InputStream input = context.getResources().getAssets().open("android.png");
DecodeError error = Assert.assertThrows(DecodeError.class, () -> {
AnimationDrawable result = JxlDecoder.loadJxl(input, null);
Drawable result = JxlDecoder.loadJxl(input, null);
});
Assert.assertEquals(DecodeError.DecodeErrorType.DecoderFailedError, error.getErrorType());
}
Expand Down Expand Up @@ -170,7 +206,7 @@ public void close() throws IOException {

// As input is marked finished, it is a decoder error and not an NeedMoreInputException
Assert.assertThrows(NoSuchFileException.class, () -> {
AnimationDrawable result = JxlDecoder.loadJxl(input, null);
Drawable result = JxlDecoder.loadJxl(input, null);
});
}

Expand All @@ -193,4 +229,37 @@ public void decodeThumbnails_shouldNotFail() throws IOException {
}

// TODO: find a way to test icc profile errors

@Test
public void decoderOptions_getBitmapConfigShouldReturnSetConfig() throws Exception {
try (JxlDecoder.Options options = new JxlDecoder.Options()) {
options.setFormat(Bitmap.Config.ARGB_8888);
Assert.assertEquals(Bitmap.Config.ARGB_8888, options.getFormat());

options.setFormat(Bitmap.Config.RGBA_F16);
Assert.assertEquals(Bitmap.Config.RGBA_F16, options.getFormat());
}
}

@Test
public void decoderOptions_setBitmapConfigInvalidConfigShouldThrowException() throws Exception {
try (JxlDecoder.Options options = new JxlDecoder.Options()) {
Assert.assertThrows(ConfigException.class, () -> options.setFormat(Bitmap.Config.ALPHA_8));
Assert.assertThrows(ConfigException.class, () -> options.setFormat(Bitmap.Config.RGB_565));
Assert.assertThrows(ConfigException.class, () -> options.setFormat(Bitmap.Config.ARGB_4444));
Assert.assertThrows(ConfigException.class, () -> options.setFormat(Bitmap.Config.HARDWARE));
Assert.assertThrows(ConfigException.class, () -> options.setFormat(Bitmap.Config.RGBA_1010102));
}
}

@Test
public void decoderOptions_getDecodeMultipleFramesShouldReturnSetConfig() throws Exception {
try (JxlDecoder.Options options = new JxlDecoder.Options()) {
options.setDecodeMultipleFrames(false);
Assert.assertFalse(options.getDecodeMultipleFrames());

options.setDecodeMultipleFrames(true);
Assert.assertTrue(options.getDecodeMultipleFrames());
}
}
}
1 change: 1 addition & 0 deletions libjxl/src/main/cpp/includes/Options.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum BitmapConfig {
class Options {
public:
BitmapConfig rgbaConfig = RGBA_8888;
bool decodeMultipleFrames = true;
};

#endif //JXLVIEWER_OPTIONS_H
30 changes: 25 additions & 5 deletions libjxl/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,42 @@ decoderOptionsGetBitmapConfig(JNIEnv * /* env */, jclass /* clazz */, jlong ptr)
return options->rgbaConfig;
}

extern "C" JNIEXPORT void JNICALL
void JNICALL
decoderOptionsSetBitmapConfig(JNIEnv * /* env */, jclass /* clazz */, jlong ptr, jint format) {
auto options = reinterpret_cast<Options *>(ptr);
options->rgbaConfig = static_cast<BitmapConfig>(format);
}


jboolean JNICALL
decoderOptionsGetDecodeMultipleFrames(JNIEnv * /* env */, jclass /* clazz */, jlong ptr) {
auto options = reinterpret_cast<Options *>(ptr);
return (options->decodeMultipleFrames) ? JNI_TRUE : JNI_FALSE;
}


void JNICALL
decoderOptionsSetDecodeMultipleFrames(JNIEnv * /* env */, jclass /* clazz */, jlong ptr,
jboolean decodeMultipleFrames) {
auto options = reinterpret_cast<Options *>(ptr);
options->decodeMultipleFrames = decodeMultipleFrames == JNI_TRUE;
}


jint registerDecoderOptions(JNIEnv *env) noexcept {
jclass classOptions = env->FindClass("fr/oupson/libjxl/JxlDecoder$Options");
if (classOptions == nullptr) {
return JNI_ERR;
}

static const JNINativeMethod methods[] = {{"alloc", "()J", reinterpret_cast<void *>(decoderOptionsAlloc)},
{"free", "(J)V", reinterpret_cast<void *>(decoderOptionsFree)},
{"setBitmapConfig", "(JI)V", reinterpret_cast<void *>(decoderOptionsSetBitmapConfig)},
{"getBitmapConfig", "(J)I", reinterpret_cast<void *>(decoderOptionsGetBitmapConfig)}};
static const JNINativeMethod methods[] = {
{"alloc", "()J", reinterpret_cast<void *>(decoderOptionsAlloc)},
{"free", "(J)V", reinterpret_cast<void *>(decoderOptionsFree)},
{"setBitmapConfig", "(JI)V", reinterpret_cast<void *>(decoderOptionsSetBitmapConfig)},
{"getBitmapConfig", "(J)I", reinterpret_cast<void *>(decoderOptionsGetBitmapConfig)},
{"getDecodeMultipleFrames", "(J)Z", reinterpret_cast<void *>(decoderOptionsGetDecodeMultipleFrames)},
{"setDecodeMultipleFrames", "(JZ)V", reinterpret_cast<void *>(decoderOptionsSetDecodeMultipleFrames)},
};

return env->RegisterNatives(classOptions, methods, sizeof(methods) / sizeof(JNINativeMethod));
}
Expand Down
28 changes: 21 additions & 7 deletions libjxl/src/main/cpp/src/Decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ Decoder::~Decoder() {
}

jobject Decoder::DecodeJxl(JNIEnv *env, InputSource &source, Options *options) {
jobject drawable = env->NewObject(drawableClass, drawableMethodID);
jobject drawable = nullptr;
BitmapConfig btmConfigNative = (options != nullptr) ? options->rgbaConfig
: BitmapConfig::RGBA_8888;

jobject bitmapConfig = (options != nullptr) ? ((options->rgbaConfig == 0)
? this->bitmapConfigRgbaU8
: this->bitmapConfigRgbaF16)
Expand Down Expand Up @@ -132,6 +133,11 @@ jobject Decoder::DecodeJxl(JNIEnv *env, InputSource &source, Options *options) {
jxlviewer::throwNewError(env, METHOD_CALL_FAILED_ERROR, "JxlDecoderGetBasicInfo");
return nullptr;
}

if (info.have_animation && (options == nullptr || options->decodeMultipleFrames)) {
drawable = env->NewObject(drawableClass, drawableMethodID);
}

if (info.alpha_bits == 0) {
if (btmConfigNative == BitmapConfig::RGBA_8888) {
out_data.setSourcePixelFormat(skcms_PixelFormat_RGB_888);
Expand Down Expand Up @@ -163,13 +169,21 @@ jobject Decoder::DecodeJxl(JNIEnv *env, InputSource &source, Options *options) {
} else if (status == JXL_DEC_FULL_IMAGE) {
AndroidBitmap_unlockPixels(env, btm);

auto btmDrawable = env->NewObject(bitmapDrawableClass, bitmapDrawableMethodID, btm);
uint32_t num = (info.animation.tps_numerator == 0) ? 1 : info.animation.tps_numerator;
env->CallVoidMethod(drawable, addDrawableMethodID, btmDrawable,
(int) (frameHeader.duration * 1000 *
info.animation.tps_denominator / num));
if (drawable != nullptr) {
auto btmDrawable = env->NewObject(bitmapDrawableClass, bitmapDrawableMethodID, btm);
uint32_t num = (info.animation.tps_numerator == 0) ? 1
: info.animation.tps_numerator;
env->CallVoidMethod(drawable, addDrawableMethodID, btmDrawable,
(int) (frameHeader.duration * 1000 *
info.animation.tps_denominator / num));
}
} else if (status == JXL_DEC_SUCCESS) {
return drawable;
if (drawable == nullptr) {
auto btmDrawable = env->NewObject(bitmapDrawableClass, bitmapDrawableMethodID, btm);
return btmDrawable;
} else {
return drawable;
}
} else if (status == JXL_DEC_FRAME) {
if (JXL_DEC_SUCCESS != JxlDecoderGetFrameHeader(dec.get(), &frameHeader)) {
jxlviewer::throwNewError(env, METHOD_CALL_FAILED_ERROR, "JxlDecoderGetFrameHeader");
Expand Down
Loading

0 comments on commit e71fceb

Please sign in to comment.