Skip to content

Commit

Permalink
Merge pull request #32 from oupson/31-library-load-images-from-inputs…
Browse files Browse the repository at this point in the history
…tream-instead-of-byte-array

 Library - Load images from inputstream instead of byte array
  • Loading branch information
oupson authored Jan 4, 2024
2 parents 665dc0f + c6307c8 commit 465de67
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 48 deletions.
17 changes: 14 additions & 3 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.drawable.AnimationDrawable
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
Expand Down Expand Up @@ -39,6 +40,7 @@ class ViewerActivity : ComponentActivity() {
private fun loadImage() {
lifecycleScope.launch(Dispatchers.IO) {
try {

val image = intent?.data?.let {
when (it.scheme) {
"https" -> {
Expand All @@ -50,7 +52,8 @@ class ViewerActivity : ComponentActivity() {

img
}
else -> loadImage(contentResolver.openInputStream(it)!!)

else -> loadImage(contentResolver.openFileDescriptor(it, "r")!!)
}
}
?: loadImage(resources.assets.open("logo.jxl"))
Expand All @@ -63,14 +66,22 @@ class ViewerActivity : ComponentActivity() {
Log.e(TAG, "Failed to load image", e)
withContext(Dispatchers.Main) {
binding.test.setImageResource(R.drawable.baseline_error_outline_24)
Toast.makeText(this@ViewerActivity, R.string.image_load_failed, Toast.LENGTH_SHORT).show()
Toast.makeText(
this@ViewerActivity,
R.string.image_load_failed,
Toast.LENGTH_SHORT
).show()
}
}
}
}

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

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

// Enable immersive mode.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
import android.content.Context;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.ParcelFileDescriptor;

import androidx.test.core.app.ApplicationProvider;

import org.junit.Assert;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Objects;

import fr.oupson.libjxl.exceptions.DecodeError;

Expand All @@ -20,15 +28,8 @@ public void decode_LogoShouldNotFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
InputStream input = context.getResources().getAssets().open("logo.jxl");

byte[] content = new byte[input.available()];

int size = input.read(content);
input.close();

Assert.assertEquals("Failed to read test file, invalid size", 117, size);

try {
AnimationDrawable result = JxlDecoder.loadJxl(content);
AnimationDrawable result = JxlDecoder.loadJxl(input);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 1, result.getNumberOfFrames());
BitmapDrawable frame = (BitmapDrawable) result.getFrame(0);
Expand All @@ -44,15 +45,8 @@ public void decode_DidiShouldNotFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
InputStream input = context.getResources().getAssets().open("didi.jxl");

byte[] content = new byte[input.available()];

int size = input.read(content);
input.close();

Assert.assertEquals("Failed to read test file, invalid size", 529406, size);

try {
AnimationDrawable result = JxlDecoder.loadJxl(content);
AnimationDrawable result = JxlDecoder.loadJxl(input);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 1, result.getNumberOfFrames());
BitmapDrawable frame = (BitmapDrawable) result.getFrame(0);
Expand All @@ -68,15 +62,35 @@ public void decode_FerrisShouldNotFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
InputStream input = context.getResources().getAssets().open("ferris.jxl");

byte[] content = new byte[input.available()];
try {
AnimationDrawable result = JxlDecoder.loadJxl(input);
Assert.assertNotNull(result);
Assert.assertEquals("Invalid number of frames", 27, result.getNumberOfFrames());

int size = input.read(content);
input.close();
for (int i = 0; i < 27; i++) {
BitmapDrawable frame = (BitmapDrawable) result.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());
}
}

Assert.assertEquals("Failed to read test file, invalid size", 404955, size);
@Test
public void decode_FerrisWithParcelFileDescriptorShouldNotFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
Path testFile = new File(context.getCacheDir(), "ferris.jxl").toPath();
if (!Files.exists(testFile)) {
try (InputStream assetInputStream = context.getResources().getAssets().open("ferris.jxl")) {
Files.copy(assetInputStream, testFile);
}
}

try {
AnimationDrawable result = JxlDecoder.loadJxl(content);
Uri androidUri = Uri.fromFile(testFile.toFile());

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

Expand All @@ -100,10 +114,8 @@ public void decode_WithoutEnoughInputShouldFail() throws IOException {
int size = input.read(content);
input.close();

// As input is marked finished, it is a decoder error and not an NeedMoreInputException
// TODO: fixme when implementing stream loading
DecodeError error = Assert.assertThrows(DecodeError.class, () -> {
AnimationDrawable result = JxlDecoder.loadJxl(content);
AnimationDrawable result = JxlDecoder.loadJxl(new ByteArrayInputStream(content));
});

Assert.assertEquals(DecodeError.DecodeErrorType.DecoderFailedError, error.getErrorType());
Expand All @@ -113,16 +125,52 @@ public void decode_WithoutEnoughInputShouldFail() throws IOException {
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);
});
Assert.assertEquals(DecodeError.DecodeErrorType.DecoderFailedError, error.getErrorType());
}

byte[] content = new byte[input.available()];
@Test
public void decode_WithFailingStreamShouldFail() throws IOException {
Context context = ApplicationProvider.getApplicationContext();

int size = input.read(content);
input.close();
class FailingStream extends InputStream {
private final InputStream wrapped;
private int count = 0;

DecodeError error = Assert.assertThrows(DecodeError.class, () -> {
AnimationDrawable result = JxlDecoder.loadJxl(content);
public FailingStream(InputStream stream) {
this.wrapped = stream;
}

@Override
public int read() {
return -1;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
if (count == 0) {
count += 1;
return wrapped.read(b, off, len);
} else {
throw new NoSuchFileException("foo.jxl");
}
}

@Override
public void close() throws IOException {
this.wrapped.close();
super.close();
}
}

InputStream input = new FailingStream(context.getResources().getAssets().open("ferris.jxl"));

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

// TODO: find a way to test icc profile errors
Expand Down
41 changes: 41 additions & 0 deletions libjxl/src/main/cpp/includes/FileDescriptorInputSource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Created by oupso on 04/01/2024.
//

#ifndef JXLVIEWER_FILEDESCRIPTORINPUTSOURCE_H
#define JXLVIEWER_FILEDESCRIPTORINPUTSOURCE_H

#include "InputSource.h"
#include <unistd.h>

inline int32_t readWithErrorHandling(JNIEnv *env, int fd, uint8_t *buffer, size_t size);

class FileDescriptorInputSource : public InputSource {
public:
FileDescriptorInputSource(JNIEnv *env, int fd) : env(env), fd(fd) {

}

virtual int32_t read(uint8_t *buffer, size_t size) {
return readWithErrorHandling(this->env, this->fd, buffer, size);
}

private:
JNIEnv *env;
int fd;
};

inline int32_t readWithErrorHandling(JNIEnv *env, int fd, uint8_t *buffer, size_t size) {
auto n = read(fd, buffer, size);
if (n > 0) {
return n;
} else if (n == 0) {
return -1;
} else {
auto error = strerror(errno);
jxlviewer::throwNewError(env, "java/io/IOException", error);
return INT32_MIN;
}
}

#endif //JXLVIEWER_FILEDESCRIPTORINPUTSOURCE_H
27 changes: 27 additions & 0 deletions libjxl/src/main/cpp/includes/InputSource.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// Created by oupso on 04/01/2024.
//

#ifndef JXLVIEWER_INPUTSOURCE_H
#define JXLVIEWER_INPUTSOURCE_H

/**
* @class InputSource
* @brief Abstraction over an input source.
*
* This represent an abstraction over a source, for example an Java InputStream or a file descriptor.
*/
class InputSource {
public:
/**
* @brief Reads data from the source into the provided buffer.
* @param buffer The buffer to read data into.
* @param size The number of bytes to read.
* @return The total number of bytes read, -1 if EOF or INT32_MIN if error with JNI error set correctly.
*
* This method reads data from the source into the specified buffer.
*/
virtual int32_t read(uint8_t *buffer, size_t size) = 0;
};

#endif //JXLVIEWER_INPUTSOURCE_H
100 changes: 100 additions & 0 deletions libjxl/src/main/cpp/includes/JniInputStream.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Created by oupso on 04/01/2024.
//

#ifndef JXLVIEWER_JNIINPUTSTREAM_H
#define JXLVIEWER_JNIINPUTSTREAM_H

#include "InputSource.h"
#include <jni.h>

#define BUFFER_SIZE (4096)

/**
* @class JniInputStream
* @brief An implementation of InputSource using Java Native Interface (JNI) to read from a Java InputStream.
*
* This class extends the InputSource class and provides functionality to read data from a Java InputStream
* using the Java Native Interface (JNI). It overrides the read method to efficiently read data from the
* underlying Java InputStream into a provided buffer.
*/
class JniInputStream : public InputSource {
public:
/**
* @brief Constructor for JniInputStream.
* @param env The JNI environment pointer.
* @param inputStream The Java InputStream object.
*
* This constructor initializes the JniInputStream with the JNI environment pointer and the Java InputStream object.
* It also sets up the JNI method ID for the read method and allocates a Java byte array for buffering.
*/
JniInputStream(JNIEnv *env, jobject inputStream) : env(env), inputStream(inputStream),
sizeRead(0) {
jclass inputStream_Clazz = env->FindClass("java/io/InputStream");
this->readMethodId = env->GetMethodID(inputStream_Clazz, "read", "([BII)I");
this->javaByteArray = env->NewByteArray(BUFFER_SIZE);
}

/**
* @brief Destructor for JniInputStream.
*
* Cleans up resources, including deleting the allocated Java byte array.
*/
~JniInputStream() {
env->DeleteLocalRef(this->javaByteArray);
}

/**
* @brief Reads data from the Java InputStream into the provided buffer.
* @param buffer The buffer to read data into.
* @param size The number of bytes to read.
* @return The total number of bytes read, -1 if EOF or INT32_MIN if error with JNI error set correctly.
*
* This method reads data from the Java InputStream into the specified buffer.
* It efficiently manages the reading process, handling buffering and JNI calls.
*/
int32_t read(uint8_t *buffer, size_t size) override {
ssize_t totalRead = (this->sizeRead > 0) ? readFromBuffer(buffer, size) : this->sizeRead;

while (totalRead < size) {
this->sizeRead = env->CallIntMethod(this->inputStream, this->readMethodId,
javaByteArray, 0, BUFFER_SIZE);
if (env->ExceptionCheck()) {
return INT32_MIN;
} else {
this->offset = 0;
if (this->sizeRead >= 0) {
totalRead += readFromBuffer(buffer + totalRead, size - totalRead);
} else {
break;
}
}
}

return totalRead;
}

private:
JNIEnv *env;
jobject inputStream;
jmethodID readMethodId;
jbyteArray javaByteArray;
size_t sizeRead;
size_t offset;

size_t readFromBuffer(const uint8_t *buffer, size_t size) {
auto res = std::min(size, sizeRead - offset);
env->GetByteArrayRegion(javaByteArray, offset,
res,
(jbyte *) buffer);
if (res + offset == sizeRead) {
sizeRead = 0;
offset = 0;
} else {
offset += res;
}
return res;
}
};

#endif //JXLVIEWER_JNIINPUTSTREAM_H
Loading

0 comments on commit 465de67

Please sign in to comment.