How to read an image file in C++ in Android with NDK
- How to read image in native layer
- Why you should read the file in native layer (or shouldn’t)
- References
- Appendix
- Image license
Software development in Android can be done using the Java SDK or Native Development Kit aka NDK made available by Android Open Source Project (AOSP). NDK is often used for writing high performance code like image processing algorithms.
Many apps have requirements to read files from disk. For reading image files, the usual approach is to read files using Java APIs that are available in Android SDK or use higher level abstractions like MediaStore APIs. I won’t cover reading different file formats in Java layer in this article.
Sometimes, there maybe need to process the image files in native layer (C++). In such cases the usual approach is to
- Load the image as a Bitmap.
- Marshall it to the native layer with JNI.
- Do read / write operations in the native layer.
However, under certain circumstances you may want to read the image directly in the native layer. If you have such circumstances - this article is for you!
FYI, When I say “native layer” or “native code” it means in C++ code. I may use these terms interchangeably in the article.
Also, while the article is primarily about reading image files in C++ - the concepts can easily be extrapolated to reading any file format in native layer in Android.
Before getting started with steps and code examples, there is yet another elephant in the room that needs to be addressed.
Why read the image in native layer to begin with?
I’ll cover it after the “how part”.
I have been told not everyone is interested in the why part that usually I go on about.
By my wife (-_-)!
Please let me know if that is indeed the case.
How to read image in native layer
If you are reading this article I expect you to be familiar with concepts like fundamentals of Android development, NDK, Java Native Interface (JNI) et cetera.
I hope you are also familiar with scoped storage concepts in Android.
Basically for improved protection to app and user data on external storage Android has tightened how applications can access files on Android. TL;DR; is without asking excessive permissions you cannot access files directly anymore. This is good for users! Good thing is you can still ask user to grant permissions to specific files like by using a file picker.
So we don’t use File
anymore. It’s more scalable to deal with Uri in Android.
Let’s start with reading image file’s Uri.
Get Uri of image to read
You can get Uri of a file using Mediastore APIs or by using file picker kind of UI.
A simple image picker can be implemented in an Activity
like this
public class MainActivity extends AppCompatActivity {
private final ActivityResultLauncher<String[]> galleryActivityLauncher
= registerForActivityResult(new ActivityResultContracts.OpenDocument(),
this::onPickImage);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
// Assuming this is called on clicking a button or something.
public void pickImage(View unused) {
galleryActivityLauncher.launch(new String[]{"image/*"});
}
private void onPickImage(Uri imageUri) {
// TODO: Load the selected image from URI.
}
}
For the rest of this article, I’ll assume that you have the Uri in hand.
Next step is to get file descriptor from this Uri.
Get file descriptor from Uri
In Unix and Unix like OS, a file descriptor (FD) is a unique identifier for a file or other IO resource like a pipe or network socket. They typically have non negative integer values. Negative values used as error values.
In Android we can use the Uri to get corresponding AssetFileDescriptor and use it to open the file in Java layer and get it’s FD
value. Once we get the native FD
value we can marshall this tiny integer value to the native layer via JNI for reading the file directly.
Important In this kind of approach, where the resource is opened by the Java layer and consumed by the Native layer, make sure that
- Java layer continues to own the file, i.e. the native layer shouldn’t close the file stream.
- Java layer continues to keep the file open until the native layer has read the file or no longer require the file to be open.
Breaking these rules can lead to unexpected race conditions.
Here’s how you’d get the native FD
in Java layer
Context context = getApplicationContext();
ContentResolver contentResolver = context.getContentResolver();
try (AssetFileDescriptor assetFileDescriptor
= contentResolver.openAssetFileDescriptor(imageUri, "r")) {
ParcelFileDescriptor parcelFileDescriptor = assetFileDescriptor.getParcelFileDescriptor();
int fd = parcelFileDescriptor.getFd();
// TODO: Read file using the fd in native layer.
// Important: Native layer shouldn't assume ownership of this fd and close it.
parcelFileDescriptor.close();
} catch (IOException ioException) {
// TODO: Handle failure scenario.
}
Marshall the FD value to native layer via JNI
For the rest of the content, I expect the readers to be familiar with
- Setting up JNI with Android.
- Basics of JNI in Android.
JNI stands for Java Native Interface - Reference to hello-jni sample from Android.
So for reading a file in native layer, we need a basic Java library and corresponding JNI file. Here’s example of Java library
/** Wrapper class for loading image in native layer. */
public final class NativeImageLoader {
static {
System.loadLibrary("image-loader-jni");
}
/** Reads the image represented by {@code fd} in native layer.
*
* <p>For apparently no reason!
*
* @return Some information about the file.
*/
public static native String readFile(int fd);
}
And let’s say we have a corresponding JNI file called image-loader-jni.cc
that is baked into the libimage-loader-jni.so
binary created by building the JNI build targets.
// image-loader-jni.cc
#include <jni.h>
// Corresponding to NativeImageLoader class in
// dev.minhazav.samples package.
extern "C" JNIEXPORT jstring JNICALL
Java_dev_minhazav_samples_NativeImageLoader_readFile(
JNIEnv* env, jclass, jint fd) {
if (fd < 0) {
return env->NewStringUTF("Invalid fd");
}
// TODO: read the image.
return env->NewStringUTF("Dummy string");
}
Read the file in native layer
And return some information about the file
There are multiple ways to handle this. I’ll list two of them
Read image with Image decoder in NDK
NDK has ImageDecoder API which can be used to read images in different formats like JPEG, PNG, GIF, WebP etc.
Pros
- It’s part of NDK, so you can
- Skip hassle of adding another third party native dependency to your project.
- Get implicit APK size reduction, by not adding third party libraries.
- Since it’s part of the platform, you get critical updates for free (without updating on your side).
- Support for several image formats and allow decoding arbitrary files opaquely.
Cons
- This was added in API level 30. So you can only target devices above this version!
- Similar to Bitmap, decodes images to one of the
Bitmap
formats (examples). By default the image is decoded inARGB_8888
format (4 bytes per pixel). - It’s an opaque library, you cannot feed in your decoder for certain file format.
Here’s how you could use it to read the image and return back some information back to Java layer.
I could have added the example doing everything in JNI code itself. But this is not stone age and we aren’t that kind of people.
We love some structure in our code. So let’s write a new library called ‘Image’.
// image.h
#include <memory>
#include <assert.h>
#include <android/imagedecoder.h>
// Data class for ARGB image (owns the memory associated with the image).
//
// Note for readers: Current implementation only allows read operations but can
// be extended to support write operations by overloading `()=` operator.
class Image {
public:
friend class ImageFactory;
// Creating the image will allocate corresponding memory.
Image(int width, int height, int channels, int stride) :
width_(width),
height_(height),
channels_(channels),
stride_(stride) {
// Restricting the image to u8 datatype for this example.
this->pixels_ = std::make_unique<uint8_t[]>(width * height * channels);
}
// Getter: Get pixel value of image at (x, y, c).
uint8_t operator()(int x, int y, int c) const {
// TODO: add assertions? (at your own risk).
uint8_t* pixel = this->pixels_.get() + (y * stride_ + x * 4 + c);
return *pixel;
}
int width() const { return this->width_; }
int height() const { return this->height_; }
int channels() const { return this->channels_; }
int stride() const { return this->stride_; }
private:
void* pixels() {
return static_cast<void*>(this->pixels_.get());
}
std::unique_ptr<uint8_t[]> pixels_;
const int width_;
const int height_;
const int channels_;
const int stride_;
};
// Factory class for creating 'Image'.
class ImageFactory {
public:
// Creates an instance of 'Image' from the file descriptor 'fd'.
//
// Will return 'nullptr' if it's unable to decode image successfully.
//
// Note for readers: If you can add abseil package to your code base, I
// recommend changing this API to return
// 'absl::StatusOr<std::unique_ptr<Image>>' instead. This will lead to much
// cleaner API and improved error handling.
static std::unique_ptr<Image> FromFd(int fd);
}
Next let’s implement the logic to decode the image from fd
. This shall be
implemented in image.cc
under ImageFactory#FromFd(..)
.
// image.cc
#include "image.h"
#include <android/imagedecoder.h>
static std::unique_ptr<Image> ImageFactory::FromFd(int fd) {
// First create decoder from fd.
AImageDecoder* decoder;
int result = AImageDecoder_createFromFd(fd, &decoder);
if (result != ANDROID_IMAGE_DECODER_SUCCESS) {
// More info: https://developer.android.com/ndk/reference/group/image-decoder#aimagedecoder_createfromfd
// Not a good idea to opaquely consume the error, it'd be a good idea to
// use StatusOr from abseil package: https://abseil.io/
return nullptr;
}
// Lambda for cleaning up the decoder when exiting.
auto decoder_cleanup = [&decoder] () {
AImageDecoder_delete(decoder);
};
const AImageDecoderHeaderInfo* header_info = AImageDecoder_getHeaderInfo(decoder);
int bitmap_format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(header_info);
// This is just for example. I don't want to handle other cases in this
// example, but that should be easy enough to do.
if (bitmap_format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
decoder_cleanup();
return nullptr;
}
constexpr int kChannels = 4;
int width = AImageDecoderHeaderInfo_getWidth(header_info);
int height = AImageDecoderHeaderInfo_getHeight(header_info);
size_t stride = AImageDecoder_getMinimumStride(decoder);
std::unique_ptr<Image> image_ptr = std::make_unique<Image>(
width, height, kChannels, stride);
size_t size = width * height * kChannels;
int decode_result = AImageDecoder_decodeImage(
decoder, image_ptr->pixels(), stride, size);
if (decode_result != ANDROID_IMAGE_DECODER_SUCCESS) {
decoder_cleanup();
return nullptr;
}
decoder_cleanup();
return image_ptr;
}
And now use this library in the JNI and read the image from fd
.
// image-loader-jni.cc
#include <string>
#include <jni.h>
#include "image.h"
extern "C" JNIEXPORT jstring JNICALL
Java_dev_minhazav_samples_NativeImageLoader_readFile(
JNIEnv* env, jclass, jint fd) {
if (fd < 0) {
return env->NewStringUTF("Invalid fd");
}
std::unique_ptr<Image> image = ImageFactory::FromFd(fd);
if (image == nullptr) {
return env->NewStringUTF("Failed to read or decode image.");
}
// Return file info as string.
std::string message = "Image load success: Dimension = "
+ std::to_string(image->width()) + "x" + std::to_string(image->height())
+ " Stride = " + std::to_string(image->stride());
return env->NewStringUTF(message.c_str());
}
Some more pointers:
- You can use APIs like AImageDecoder_setTargetSize to read rescaled image directly.
- Similarly, you can use API like AImageDecoder_setCrop to crop before consumption.
Although, in practice I found AImageDecoder_setTargetSize to be slower than I’d expect a down-sampling operation to be. If the performance of this API concerns you and you have other approach in hand, try loading the full resolution image and down-sampling Image
separately.
With the solution so far, you can get a working version of image decoding in native layer.
Reasons to read further:
- You want to decode image in native layer but you have lot of customers using Android <= API 30.
- You want to read something other than an image.
- You have custom & better decoder of your own.
- You are a curious reader, you knowledge hog!
- We sill have the pending Mr. Elephant in the room to address.
Read image with custom decoders
The following approach can be used to read any file using fd
value and then
you can use your own decoder to decode the image.
For the purpose of this example, I’ll assume you have some decoder of your own
and it’s implemented underneath ImageFactory
implementation. Let’s assume an
interface.
class ImageFactory {
public:
// Decodes the `image_buffer` and returns Image instance.
//
// Suggestion for readers: Use absl::string_view instead of string here.
static std::unique_ptr<Image> FromString(const std::string& image_buffer);
}
Reading file the Unix way!
For this approach, before even going into the native layer details, we need two additional information from the java layer.
start offset
of thefd
(Very likely0
unless you don’t want to read the file from beginning). You can get this using AssetFileDescriptor#getStartOffset() API.length
of the file. You can get this using AssetFileDescriptor#getLength() API.
After you get this info in the Java layer, marshall it to the native layer via JNI. For the following example I’ll assume you want to decode an image file and your decoder can handle it.
// In the JNI (image-loader-jni.cc)
// Read the image
std::unique_ptr<Image> image = nullptr;
{
std::string image_buffer;
image_buffer.resize(fd_length);
int remaining_length = read(fd, &image_buffer[0], length);
if (remaining_length != 0) {
return env->NewStringUTF("Failed to read full image");
}
image = ImageFactory::FromString(image_buffer);
}
If your decoder supports buffered data, you could read the image file in buffers as well, with the approach above.
Why you should read the file in native layer (or shouldn’t)
Well, why you may want to read the file in native layer instead of Java layer is your problem.
However, I would like to demystify a few misconception based on my experience using examples of different use-cases.
Consuming image in native layer
If your concern is high latency of reading or decoding image in Java layer.. Note that Android Java SDK also comes with ImageDecoder also have a Java API which is likely backed by the same native implementation. You can use these APIs to read images as Drawable or Bitmap.
For any kind of post-processing you might want to do in the native layer, you can easily marshall Bitmap
reference to the native layer. NDK has good support for Bitmap and it allows you to process them between Java and Native with little overhead.
I plan to write more about it in a separate article.
You might not get latency benefit by using ImageDecoder
in the native layer as compared to the Java layer. You may get latency benefits if you have some decoder implementation that can handle decoding faster that what NDK library does.
One valid reason to still read the file in native layer might be to get rid of holding nasty Bitmaps
in Java layer when you don’t need to.
For example if you just want to read an image, do some post processing, encode it as jpeg and save to disk - you can avoid holding a Bitmap
reference in Java.
I have often found holding large Bitmaps
can lead to visible performance issues likely because we have to rely on GC to reclaim the memory held by Bitmap
when they are no longer referenced. GC may not always work in predictable fashion. However, Bitmap#recycle() API might help you with this as well.
Avoiding marshalling of file data across JNI boundary
It’s a good idea to use native approach if
You need to read alternative file format and you have your custom decoder implementation for it. This way you can avoid first reading it in the Java layer as String
and later marshalling it to the native layer via JNI.
I am not 100% sure how exactly data marshalling works across Java Native boundary but it is #1 tip around JNI by Android developer website to avoid marshalling large data.
It “might” be more performant to pass fd
to native layer instead.
Consume C++ only libraries
This is similar to the point above. If you have third party libraries for decoding your image or custom file format which doesn’t come in Java variant or the pure Java variant is less performant - it’d be a good idea to use the shared approach.
You like C++ more than Java
No comments, I hear you! Do as you see fit - for this world is thy canvas!
References
- File descriptor - Wikipedia
- AssetFileDescriptor - Android documentation
- Reference to hello-jni sample from Android
- NDK guide on ImageDecoder
- JNI tips by Android developer website
Appendix
Some more resources in case you get stuck at any of these steps.
fatal error: ‘imagedecoder.h’ file not found
So you stumbled upon this too! If you spent a lot of hours at this, let me know over comments as I did too! Let’s share the misery :)
There can be a couple of reasons why you are facing this.
1. You didn’t like the right target library
If you are using CMake
based approach, add jnigraphics
to target_link_libraries
.
In the above example, it would look like
target_link_libraries( # Specifies the target library.
image-loader-jni
${log-lib}
jnigraphics)
2. You didn’t include it right
This is what I didn’t realize.
The library path is android/imagedecoder.h
and not imagedecoder.h
. So include correctly
#include <jni.h>
#include <android/imagedecoder.h>
If this doesn’t help either, make sure you are targeting min SDK version to be >= 30.
Image license
Image generated using stable diffusion is free to use under CreativeML Open RAIL-M by huggingface.co.
Want to read more such similar contents?
I like to write articles on topic less covered on internet. They revolve around writing fast algorithms, image processing as well as general software engineering.
I publish many of them on Medium.
If you are already on medium - Please join 4200+ other members and Subscribe to my articles to get updates as I publish.
If you are not on Medium - Medium has millions of amazing articles from 100K+ authors. To get access to those, please join using my referral link. This will give you access to all the benefits of Medium and Medium shall pay me a piece to support my writing!
Thanks!