Android native image loader rewritten from scratch
* Now supports reading directly from Java InputStream instead of having to read image file into memory first * Optimized native code - reduced unneccessary memory copies
This commit is contained in:
parent
b83603cd8f
commit
7057e9cb18
@ -8,6 +8,7 @@ LOCAL_MODULE := stbijme
|
||||
|
||||
LOCAL_C_INCLUDES += $(LOCAL_PATH)
|
||||
|
||||
LOCAL_CFLAGS := -std=c99
|
||||
LOCAL_LDLIBS := -lz -llog -Wl,-s
|
||||
|
||||
LOCAL_SRC_FILES := com_jme3_texture_plugins_AndroidNativeImageLoader.c
|
||||
|
@ -1,25 +1,332 @@
|
||||
#include "com_jme3_texture_plugins_AndroidNativeImageLoader.h"
|
||||
// for __android_log_print(ANDROID_LOG_INFO, "YourApp", "formatted message");
|
||||
#include <android/log.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef DEBUG
|
||||
#include <android/log.h>
|
||||
#define LOGI(fmt, ...) __android_log_print(ANDROID_LOG_INFO, \
|
||||
"NativeImageLoader", fmt, ##__VA_ARGS__);
|
||||
#else
|
||||
#define LOGI(fmt, ...)
|
||||
#endif
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_NO_STDIO
|
||||
#define STBI_NO_HDR
|
||||
#include "stb_image.h"
|
||||
|
||||
typedef unsigned int uint32;
|
||||
typedef struct
|
||||
{
|
||||
JNIEnv* env;
|
||||
jbyteArray tmp;
|
||||
int tmpSize;
|
||||
jobject isObject;
|
||||
jmethodID isReadMethod;
|
||||
jmethodID isSkipMethod;
|
||||
int isEOF;
|
||||
char* errorMsg;
|
||||
}
|
||||
JavaInputStreamWrapper;
|
||||
|
||||
static void throwIOException(JNIEnv* env, const char* message)
|
||||
{
|
||||
jclass ioExClazz = (*env)->FindClass(env, "java/io/IOException");
|
||||
(*env)->ThrowNew(env, ioExClazz, message);
|
||||
}
|
||||
|
||||
static int InputStream_read(void *user, char *nativeData, int nativeSize) {
|
||||
JavaInputStreamWrapper* wrapper = (JavaInputStreamWrapper*) user;
|
||||
JNIEnv* env = wrapper->env;
|
||||
|
||||
if (nativeSize <= 0)
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = "read() requested negative or zero size";
|
||||
return 0;
|
||||
}
|
||||
|
||||
jbyteArray tmp = wrapper->tmp;
|
||||
jint tmpSize = wrapper->tmpSize;
|
||||
jint remaining = nativeSize;
|
||||
jint offset = 0;
|
||||
|
||||
while (offset < nativeSize)
|
||||
{
|
||||
// Read data into Java array.
|
||||
jint toRead = tmpSize < remaining ? tmpSize : remaining;
|
||||
jint read = (*env)->CallIntMethod(env, wrapper->isObject,
|
||||
wrapper->isReadMethod,
|
||||
tmp, (jint)0, (jint)toRead);
|
||||
|
||||
// Check IOException
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = NULL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOGI("InputStream->read(tmp, 0, %d) = %d", toRead, read);
|
||||
|
||||
// Read -1 bytes = EOF.
|
||||
if (read < 0)
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = NULL;
|
||||
break;
|
||||
}
|
||||
else if (read == 0)
|
||||
{
|
||||
// Read 0 bytes, give it another try.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read 1 byte or more.
|
||||
|
||||
LOGI("memcpy(native[%d], java, %d)", offset, read);
|
||||
|
||||
// Copy contents of Java array to native array.
|
||||
jbyte* nativeTmp = (*env)->GetPrimitiveArrayCritical(env, tmp, 0);
|
||||
|
||||
if (nativeTmp == NULL)
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = "Failed to acquire Java array contents";
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(&nativeData[offset], nativeTmp, read);
|
||||
|
||||
(*env)->ReleasePrimitiveArrayCritical(env, tmp, nativeTmp, 0);
|
||||
|
||||
offset += read;
|
||||
remaining -= read;
|
||||
|
||||
assert(remaining >= 0);
|
||||
assert(offset <= nativeSize);
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
static void InputStream_skip(void *user, int n) {
|
||||
JavaInputStreamWrapper* wrapper = (JavaInputStreamWrapper*) user;
|
||||
JNIEnv* env = wrapper->env;
|
||||
|
||||
if (n < 0)
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = "Negative seek attempt detected";
|
||||
return;
|
||||
}
|
||||
else if (n == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// InputStream.skip(n);
|
||||
jlong result = (*env)->CallLongMethod(env, wrapper->isObject,
|
||||
wrapper->isSkipMethod, (jlong)n);
|
||||
|
||||
LOGI("InputStream->skip(%lld) = %lld", (jlong)n, result);
|
||||
|
||||
// IOException
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = NULL;
|
||||
}
|
||||
else if ((int)result != n)
|
||||
{
|
||||
wrapper->isEOF = 1;
|
||||
wrapper->errorMsg = "Could not skip requested number of bytes";
|
||||
}
|
||||
}
|
||||
|
||||
static int InputStream_eof(void *user) {
|
||||
JavaInputStreamWrapper* wrapper = (JavaInputStreamWrapper*) user;
|
||||
LOGI("InputStream->eof() = %s", wrapper->isEOF ? "true" : "false");
|
||||
return wrapper->isEOF;
|
||||
}
|
||||
|
||||
static stbi_io_callbacks JavaInputStreamCallbacks ={
|
||||
InputStream_read,
|
||||
InputStream_skip,
|
||||
InputStream_eof,
|
||||
};
|
||||
|
||||
static JavaInputStreamWrapper createInputStreamWrapper(JNIEnv* env, jobject is, jbyteArray tmpArray)
|
||||
{
|
||||
JavaInputStreamWrapper wrapper;
|
||||
jclass inputStreamClass = (*env)->FindClass(env, "java/io/InputStream");
|
||||
|
||||
wrapper.env = env;
|
||||
wrapper.isObject = is;
|
||||
wrapper.isEOF = 0;
|
||||
wrapper.errorMsg = NULL;
|
||||
wrapper.isReadMethod = (*env)->GetMethodID(env, inputStreamClass, "read", "([BII)I");
|
||||
wrapper.isSkipMethod = (*env)->GetMethodID(env, inputStreamClass, "skip", "(J)J");
|
||||
wrapper.tmp = (jbyteArray) tmpArray;
|
||||
wrapper.tmpSize = (*env)->GetArrayLength(env, tmpArray);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
static jobject createJmeImage(JNIEnv* env, int width, int height, int comps, char* data)
|
||||
{
|
||||
// Convert # of components to jME format.
|
||||
jclass formatClass = (*env)->FindClass(env, "com/jme3/texture/Image$Format");
|
||||
jfieldID formatFieldID;
|
||||
|
||||
switch (comps)
|
||||
{
|
||||
case 1:
|
||||
formatFieldID = (*env)->GetStaticFieldID(env, formatClass,
|
||||
"Luminance8", "Lcom/jme3/texture/Image$Format;");
|
||||
break;
|
||||
case 2:
|
||||
formatFieldID = (*env)->GetStaticFieldID(env, formatClass,
|
||||
"Luminance8Alpha8", "Lcom/jme3/texture/Image$Format;");
|
||||
break;
|
||||
case 3:
|
||||
formatFieldID = (*env)->GetStaticFieldID(env, formatClass,
|
||||
"RGB8", "Lcom/jme3/texture/Image$Format;");
|
||||
break;
|
||||
case 4:
|
||||
formatFieldID = (*env)->GetStaticFieldID(env, formatClass,
|
||||
"RGBA8", "Lcom/jme3/texture/Image$Format;");
|
||||
break;
|
||||
default:
|
||||
throwIOException(env, "Unrecognized number of components");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
jobject formatVal = (*env)->GetStaticObjectField(env, formatClass, formatFieldID);
|
||||
|
||||
// Get colorspace sRGB
|
||||
jclass colorSpaceClass = (*env)->FindClass(env, "com/jme3/texture/image/ColorSpace");
|
||||
jfieldID sRGBFieldID = (*env)->GetStaticFieldID(env, colorSpaceClass,
|
||||
"sRGB", "Lcom/jme3/texture/image/ColorSpace;");
|
||||
jobject sRGBVal = (*env)->GetStaticObjectField(env, colorSpaceClass, sRGBFieldID);
|
||||
|
||||
int size = width * height * comps;
|
||||
|
||||
// Stick it in a ByteBuffer
|
||||
jobject directBuffer = (*env)->NewDirectByteBuffer(env, data, size);
|
||||
|
||||
if (directBuffer == NULL)
|
||||
{
|
||||
throwIOException(env, "Failed to allocate ByteBuffer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create JME image.
|
||||
jclass jmeImageClass = (*env)->FindClass(env, "com/jme3/texture/Image");
|
||||
|
||||
// Image(Format format, int width, int height, ByteBuffer data, ColorSpace colorSpace)
|
||||
jmethodID newImageMethod = (*env)->GetMethodID(env, jmeImageClass, "<init>",
|
||||
"(Lcom/jme3/texture/Image$Format;IILjava/nio/ByteBuffer;Lcom/jme3/texture/image/ColorSpace;)V");
|
||||
|
||||
jobject jmeImage = (*env)->NewObject(env, jmeImageClass, newImageMethod,
|
||||
formatVal, (jint)width, (jint)height,
|
||||
directBuffer, sRGBVal);
|
||||
|
||||
return jmeImage;
|
||||
}
|
||||
|
||||
static void flipImage(int scanline, int height, char* data)
|
||||
{
|
||||
char tmp[scanline];
|
||||
|
||||
for (int y = 0; y < height / 2; y++)
|
||||
{
|
||||
int oppY = height - y - 1;
|
||||
int yOff = y * scanline;
|
||||
int oyOff = oppY * scanline;
|
||||
// Copy scanline at Y to tmp
|
||||
memcpy(tmp, &data[yOff], scanline);
|
||||
// Copy data at opposite Y to Y
|
||||
memcpy(&data[yOff], &data[oyOff], scanline);
|
||||
// Copy tmp to opposite Y
|
||||
memcpy(&data[oyOff], tmp, scanline);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_load
|
||||
(JNIEnv * env, jobject thisObj, jobject inputStream, jboolean flipY, jbyteArray tmpArray)
|
||||
{
|
||||
JavaInputStreamWrapper wrapper = createInputStreamWrapper(env, inputStream, tmpArray);
|
||||
stbi_uc* imageData;
|
||||
int width, height, comps;
|
||||
|
||||
LOGI("stbi_load_from_callbacks");
|
||||
|
||||
imageData = stbi_load_from_callbacks(&JavaInputStreamCallbacks, &wrapper, &width, &height, &comps, STBI_default);
|
||||
|
||||
if ((*env)->ExceptionCheck(env))
|
||||
{
|
||||
// IOException
|
||||
goto problems;
|
||||
}
|
||||
else if (wrapper.errorMsg != NULL)
|
||||
{
|
||||
// Misc error
|
||||
throwIOException(env, wrapper.errorMsg);
|
||||
goto problems;
|
||||
}
|
||||
else if (imageData == NULL)
|
||||
{
|
||||
// STBI error
|
||||
throwIOException(env, stbi_failure_reason());
|
||||
goto problems;
|
||||
}
|
||||
|
||||
// No IOExceptions or errors encountered. We have image data!
|
||||
|
||||
// Maybe we need to flip it.
|
||||
LOGI("Flipping image");
|
||||
if (flipY)
|
||||
{
|
||||
flipImage(width * comps, height, imageData);
|
||||
}
|
||||
|
||||
// Create the jME3 image.
|
||||
LOGI("Creating jME3 image");
|
||||
return createJmeImage(env, width, height, comps, imageData);
|
||||
|
||||
problems:
|
||||
if (imageData != NULL)
|
||||
{
|
||||
stbi_image_free(imageData);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getFailureReason
|
||||
(JNIEnv * env, jclass clazz)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getImageInfo
|
||||
(JNIEnv * env, jclass clazz, jobject inBuffer, jint bufSize, jobject outBuffer, jint outSize)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_decodeBuffer
|
||||
(JNIEnv * env, jclass clazz, jobject inBuffer, jint inSize, jboolean flipY, jobject outBuffer, jint outSize)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
JNIEXPORT jobject JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getFailureReason
|
||||
(JNIEnv * env, jclass clazz)
|
||||
{
|
||||
return stbi_failure_reason();
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_getImageInfo
|
||||
(JNIEnv * env, jclass clazz, jobject inBuffer, jint bufSize, jobject outBuffer, jint outSize)
|
||||
{
|
||||
@ -124,3 +431,4 @@ JNIEXPORT jint JNICALL Java_com_jme3_texture_plugins_AndroidNativeImageLoader_de
|
||||
|
||||
// stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp);
|
||||
}
|
||||
*/
|
||||
|
@ -5,13 +5,8 @@ import com.jme3.asset.AssetLoadException;
|
||||
import com.jme3.asset.AssetLoader;
|
||||
import com.jme3.asset.TextureKey;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.image.ColorSpace;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
@ -20,145 +15,28 @@ import java.util.logging.Logger;
|
||||
* loading. This loader does not.
|
||||
*
|
||||
* @author iwgeric
|
||||
* @author Kirill Vainer
|
||||
*/
|
||||
public class AndroidNativeImageLoader implements AssetLoader {
|
||||
private static final Logger logger = Logger.getLogger(AndroidNativeImageLoader.class.getName());
|
||||
|
||||
public Image load(InputStream in, boolean flipY) throws IOException{
|
||||
int result;
|
||||
byte[] bytes = getBytes(in);
|
||||
int origSize = bytes.length;
|
||||
// logger.log(Level.INFO, "png file length: {0}", size);
|
||||
private final byte[] tmpArray = new byte[1024];
|
||||
|
||||
ByteBuffer origDataBuffer = BufferUtils.createByteBuffer(origSize);
|
||||
origDataBuffer.clear();
|
||||
origDataBuffer.put(bytes, 0, origSize);
|
||||
origDataBuffer.flip();
|
||||
|
||||
int headerSize = 12;
|
||||
ByteBuffer headerDataBuffer = BufferUtils.createByteBuffer(headerSize);
|
||||
headerDataBuffer.asIntBuffer();
|
||||
headerDataBuffer.clear();
|
||||
|
||||
result = getImageInfo(origDataBuffer, origSize, headerDataBuffer, headerSize);
|
||||
if (result != 0) {
|
||||
logger.log(Level.SEVERE, "Image header could not be read: {0}", getFailureReason());
|
||||
return null;
|
||||
}
|
||||
headerDataBuffer.rewind();
|
||||
|
||||
// logger.log(Level.INFO, "image header size: {0}", headerDataBuffer.capacity());
|
||||
// int position = 0;
|
||||
// while (headerDataBuffer.position() < headerDataBuffer.capacity()) {
|
||||
// int value = headerDataBuffer.getInt();
|
||||
// logger.log(Level.INFO, "position: {0}, value: {1}",
|
||||
// new Object[]{position, value});
|
||||
// position++;
|
||||
// }
|
||||
// headerDataBuffer.rewind();
|
||||
|
||||
|
||||
int width = headerDataBuffer.getInt();
|
||||
int height = headerDataBuffer.getInt();
|
||||
int numComponents = headerDataBuffer.getInt();
|
||||
int imageDataSize = width * height * numComponents;
|
||||
// logger.log(Level.INFO, "width: {0}, height: {1}, numComponents: {2}, imageDataSize: {3}",
|
||||
// new Object[]{width, height, numComponents, imageDataSize});
|
||||
|
||||
ByteBuffer imageDataBuffer = BufferUtils.createByteBuffer(imageDataSize);
|
||||
imageDataBuffer.clear();
|
||||
|
||||
result = decodeBuffer(origDataBuffer, origSize, flipY, imageDataBuffer, imageDataSize);
|
||||
if (result != 0) {
|
||||
logger.log(Level.SEVERE, "Image could not be decoded: {0}", getFailureReason());
|
||||
return null;
|
||||
}
|
||||
imageDataBuffer.rewind();
|
||||
|
||||
// logger.log(Level.INFO, "png outSize: {0}", imageDataBuffer.capacity());
|
||||
// int pixelNum = 0;
|
||||
// while (imageDataBuffer.position() < imageDataBuffer.capacity()) {
|
||||
// short r = (short) (imageDataBuffer.get() & 0xFF);
|
||||
// short g = (short) (imageDataBuffer.get() & 0xFF);
|
||||
// short b = (short) (imageDataBuffer.get() & 0xFF);
|
||||
// short a = (short) (imageDataBuffer.get() & 0xFF);
|
||||
// logger.log(Level.INFO, "pixel: {0}, r: {1}, g: {2}, b: {3}, a: {4}",
|
||||
// new Object[]{pixelNum, r, g, b, a});
|
||||
// pixelNum++;
|
||||
// }
|
||||
// imageDataBuffer.rewind();
|
||||
|
||||
BufferUtils.destroyDirectBuffer(origDataBuffer);
|
||||
BufferUtils.destroyDirectBuffer(headerDataBuffer);
|
||||
|
||||
Image img = new Image(getImageFormat(numComponents), width, height, imageDataBuffer, ColorSpace.sRGB);
|
||||
|
||||
return img;
|
||||
static {
|
||||
System.loadLibrary("stbijme");
|
||||
}
|
||||
|
||||
private static native Image load(InputStream in, boolean flipY, byte[] tmpArray) throws IOException;
|
||||
|
||||
public Image load(AssetInfo info) throws IOException {
|
||||
// logger.log(Level.INFO, "Loading texture: {0}", ((TextureKey)info.getKey()).toString());
|
||||
boolean flip = ((TextureKey) info.getKey()).isFlipY();
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = info.openStream();
|
||||
Image img = load(in, flip);
|
||||
if (img == null){
|
||||
throw new AssetLoadException("The given image cannot be loaded " + info.getKey());
|
||||
}
|
||||
return img;
|
||||
return load(info.openStream(), flip, tmpArray);
|
||||
} finally {
|
||||
if (in != null){
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Image.Format getImageFormat(int stbiNumComponents) {
|
||||
// stb_image always returns 8 bit components
|
||||
// N=#comp components
|
||||
// 1 grey
|
||||
// 2 grey, alpha
|
||||
// 3 red, green, blue
|
||||
// 4 red, green, blue, alpha
|
||||
Image.Format format = null;
|
||||
|
||||
if (stbiNumComponents == 1) {
|
||||
format = Image.Format.Luminance8;
|
||||
} else if (stbiNumComponents == 2) {
|
||||
format = Image.Format.Luminance8Alpha8;
|
||||
} else if (stbiNumComponents == 3) {
|
||||
format = Image.Format.RGB8;
|
||||
} else if (stbiNumComponents == 4) {
|
||||
format = Image.Format.RGBA8;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Format returned by stbi is not valid. Returned value: " + stbiNumComponents);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
public static byte[] getBytes(InputStream input) throws IOException {
|
||||
byte[] buffer = new byte[32768];
|
||||
int bytesRead;
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
while ((bytesRead = input.read(buffer)) != -1)
|
||||
{
|
||||
os.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
byte[] output = os.toByteArray();
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Load jni .so on initialization */
|
||||
static {
|
||||
System.loadLibrary("stbijme");
|
||||
}
|
||||
|
||||
private static native int getImageInfo(ByteBuffer inBuffer, int inSize, ByteBuffer outBuffer, int outSize);
|
||||
private static native int decodeBuffer(ByteBuffer inBuffer, int inSize, boolean flipY, ByteBuffer outBuffer, int outSize);
|
||||
private static native String getFailureReason();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user