Many developers appear to embed libpng with their NDK project in order to decode PNGs. While libpng does offer great flexibility, the amount of code necessary to decode an image is surprisingly high, and the additional work needed to maintain a libpng build means that most of the time, using the system’s decoding routines is perfectly reasonable.
But wait, isn’t the NDK for C++ development only? True, but usually we are still running in a virtual machine that has access to a large panel of high-level utility libraries. This article actually demonstrates a broader, useful technique I call return-to-JVM that you can use for other purposes than simply PNG loading.
I suggest putting your PNG files in the assets directory of your application, so that they can be accessed by path.
First, let’s decide of a Java class and object that will act as a PNG factory and manager for us. Let’s call it PngManager:
import android.content.res.AssetManager;
public class PngManager
{
private AssetManager amgr;
public Bitmap open(String path)
{
try
{
return BitmapFactory.decodeStream(amgr.open(path));
}
catch (Exception e) { }
return null;
}
public int getWidth(Bitmap bmp) { return bmp.getWidth(); }
public int getHeight(Bitmap bmp) { return bmp.getHeight(); }
public void getPixels(Bitmap bmp, int[] pixels)
{
int w = bmp.getWidth();
int h = bmp.getHeight();
bmp.getPixels(pixels, 0, w, 0, 0, w, h);
}
public void close(Bitmap bmp)
{
bmp.recycle();
}
}
Now to load the PNG from the C++ part of the program, use the following code:
jobject g_pngmgr;
JNIEnv *g_env;
/* ... */
char const *path = "images/myimage.png";
jclass cls = g_env->GetObjectClass(g_pngmgr);
jmethodID mid;
/* Ask the PNG manager for a bitmap */
mid = g_env->GetMethodID(cls, "open",
"(Ljava/lang/String;)Landroid/graphics/Bitmap;");
jstring name = g_env->NewStringUTF(path);
jobject png = g_env->CallObjectMethod(g_pngmgr, mid, name);
g_env->DeleteLocalRef(name);
g_env->NewGlobalRef(png);
/* Get image dimensions */
mid = g_env->GetMethodID(cls, "getWidth", "(Landroid/graphics/Bitmap;)I");
int width = g_env->CallIntMethod(g_pngmgr, mid, png);
mid = g_env->GetMethodID(cls, "getHeight", "(Landroid/graphics/Bitmap;)I");
int height = g_env->CallIntMethod(g_pngmgr, mid, png);
/* Get pixels */
jintArray array = g_env->NewIntArray(width * height);
g_env->NewGlobalRef(array);
mid = g_env->GetMethodID(cls, "getPixels", "(Landroid/graphics/Bitmap;[I)V");
g_env->CallVoidMethod(g_pngmgr, mid, png, array);
jint *pixels = g_env->GetIntArrayElements(array, 0);
Now do anything you want with the pixels, for instance bind them to a texture.
And to release the bitmap when finished:
g_env->ReleaseIntArrayElements(array, pixels, 0);
g_env->DeleteGlobalRef(array);
/* Free image */
mid = g_env->GetMethodID(cls, "close", "(Landroid/graphics/Bitmap;)V");
g_env->CallVoidMethod(g_pngmgr, mid, png);
g_env->DeleteGlobalRef(png);
This will not work out of the box. There are a few last things to do, which will hugely depend on your global application architecture and are thus left as an exercise to the reader:
- Store an AssetManager object in PngManager::amgr before the first call to open() is made (for instance by calling Activity::getAssets() upon application initialisation).
- Store in g_env a valid JNIEnv * value (the JNI environment is the first argument to all JNI methods), either by remembering it or by using jvm->AttachCurrentThread().
- Store in g_pngmgr a valid jobject handle to a PngManager instance (for instance by calling a JNI method with the instance as an argument).
- Error checking was totally omitted from the code for the sake of clarity.
- Some of the dynamically retrieved variables could benefit from being cached.
I hope this can prove helpful!
For a C++-only solution to this problem, see Load pngs from assets in NDK by Bill Hsu.