I'm passing window buffer from native side in android app to java side.
AndroidBitmapInfo info;
void saveBufferToBitmap(JNIEnv *env, ANativeWindow_Buffer *buffer, jobject bitmap) {
void *pixels;
LOGI(10, "saving buffer to bitmap");
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
LOGE(10, "Failed to get bitmap info");
return;
}
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
LOGE(10, "Failed to lock pixles for bitmap");
return;
}
int i, scan_length;
scan_length = buffer->width * 4;
memcpy(pixels, buffer->bits, buffer->width * buffer->height * 4); // 4 = (rgba)
AndroidBitmap_unlockPixels(env, bitmap);
//free(pixels); // here
}
Should i free pixels buffer in // here ? Does AndroidBitmap_lockPixels/AndroidBitmap_unlockPixels copy buffer to bitmap?
As a general rule you generally should never free a pointer that you did not create with new yourself. The library call you get the pointer from may use whatever allocation or just pass out a pointer to an internal data structure. The second is very likely in this case.
Looking at the documentation from the source:
/**
* Given a java bitmap object, attempt to lock the pixel address.
* Locking will ensure that the memory for the pixels will not move
* until the unlockPixels call, and ensure that, if the pixels had been
* previously purged, they will have been restored.
*
* If this call succeeds, it must be balanced by a call to
* AndroidBitmap_unlockPixels, after which time the address of the pixels should
* no longer be used.
*
* If this succeeds, *addrPtr will be set to the pixel address. If the call
* fails, addrPtr will be ignored.
*/
int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);
Source: https://android.googlesource.com/platform/frameworks/native/+/master/include/android/bitmap.h
That tells us not do anything with pixels after AndroidBitmap_unlockPixels and definitely not free it.
Related
I need to convert an image to grayscale and then back to RGBA to be able to draw in it.
Currently, I am doing it with two different cvtColor calls, which works fine, although the performance is not good in Android (RGBA -> GRAY -> RGBA).
Getting a gray image from the camera directly is faster and only having to do one cvtColor call makes a huge difference (GRAY -> RGBA).
The problem is that the second method makes the app close after a few seconds. The logcat in Android Studio does not show a crash for the app, but it shows some errors with the No Filters option selected. Here is the log https://pastebin.com/jA7jFSvu. It seems to point to a problem with OpenCV's camera.
Below are the two different pieces of code.
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {
// Method 1 - works
cameraImage = inputFrame.rgba();
native.exampleProcessImage1(cameraImage.getNativeObjAddr(), cameraImage.getNativeObjAddr());
return cameraImage;
// Method 2 - app closes after a few seconds
cameraImage = inputFrame.gray();
Mat result = new Mat();
native.exampleProcessImage2(cameraImage.getNativeObjAddr(), result.getNativeObjAddr());
return result;
}
And this is my code in C++:
void Java_com_example_native_exampleProcessImage1(JNIEnv *env, jobject instance, jlong sourceImage, jlong destImage) {
// works!
Mat &src = * ((Mat *) sourceImage);
Mat &dest = * ((Mat *) destImage);
Mat pivot;
// src is RGBA
cvtColor(src, pivot, COLOR_RGBA2GRAY);
cvtColor(pivot, dest, COLOR_GRAY2RGBA);
// dest is RGBA
// process
}
void Java_com_example_native_exampleProcessImage2(JNIEnv *env, jobject instance, jlong sourceImage, jlong destImage) {
// does not work
Mat &src = * ((Mat *) sourceImage);
Mat &dest = * ((Mat *) destImage);
// src is GRAY
cvtColor(src, dest, COLOR_GRAY2RGBA);
// dest is RGBA
// process
}
This works as expected on Linux and OpenCV.
Do you know what I am doing wrong? Is there another way to achieve the same? Performance is key, in particular for Android devices.
Thank you in advance.
For second case you have memory leak and this leads to leak
~ 3 sec * fps * frame_resolution * 4 bytes
I think crash is happening after the memory is full.
You need to call result.release(); somewhere after each exampleProcessImage2 call
On API Level 19+ devices, we have getByteCount() and getAllocationByteCount(), each of which return a size of the Bitmap in bytes. The latter takes into account the fact that a Bitmap can actually represent a smaller image than its byte count (e.g., the Bitmap originally held a larger image, but then was used with BitmapFactory.Options and inBitmap to hold a smaller image).
In most Android IPC scenarios, particularly those involving Parcelable, we have a 1MB "binder transaction limit".
For the purposes of determining whether a given Bitmap is small enough for IPC, do we use getByteCount() or getAllocationByteCount()?
My gut instinct says that we use getByteCount(), as that should be the number of bytes the current image in the Bitmap is taking up, but I was hoping somebody had a more authoritative answer.
The size of the image data written to the parcel is getByteCount() plus the size of the Bitmap's color table, if it has one. There are also approximately 48 bytes of Bitmap attributes written to the parcel. The following code analysis and tests provide the basis for these statements.
The native function to write a Bitmap to a Parcel begins at line 620 of this file. The function is included here with explanation added:
static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
jlong bitmapHandle,
jboolean isMutable, jint density,
jobject parcel) {
const SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle);
if (parcel == NULL) {
SkDebugf("------- writeToParcel null parcel\n");
return JNI_FALSE;
}
android::Parcel* p = android::parcelForJavaObject(env, parcel);
The following seven ints are the first data written to the parcel. In Test #2, described below, these values are read from a sample parcel to confirm the size of data written for the Bitmap.
p->writeInt32(isMutable);
p->writeInt32(bitmap->colorType());
p->writeInt32(bitmap->alphaType());
p->writeInt32(bitmap->width());
p->writeInt32(bitmap->height());
p->writeInt32(bitmap->rowBytes());
p->writeInt32(density);
If the bitmap has a color map, it is written to the parcel. A precise determination the parcel size of a bitmap must address this also.
if (bitmap->colorType() == kIndex_8_SkColorType) {
SkColorTable* ctable = bitmap->getColorTable();
if (ctable != NULL) {
int count = ctable->count();
p->writeInt32(count);
memcpy(p->writeInplace(count * sizeof(SkPMColor)),
ctable->lockColors(), count * sizeof(SkPMColor));
ctable->unlockColors();
} else {
p->writeInt32(0); // indicate no ctable
}
}
Now we get to the core of the question: How much data is written for the bitmap image? The amount is determined by this call to bitmap->getSize(). That function is analyzed below. Note here that the value is stored in size, which is used in the code following to write the blob for the image data, and copy the data into the memory pointed to by the blob.
size_t size = bitmap->getSize();
A variable size block of data is written to a parcel using a blob. If the block is less than 40K, it is written "in place" in the parcel. Blocks larger than 40K are written to shared memory using ashmem, and the attributes of the ashmem region are written in the parcel. The blob itself is just a small descriptor containing a few members that include a pointer to the block, its length, and a flag indicating if the block is in-place or in shared memory. The class definition for WriteableBlob is at line 262 of this file. The definition of writeBlob() is at line 747 of this file. writeBlob() determines if the data block is small enough to be written in-place. If so, it expands the parcel buffer to make room. If not, an ashmem region is created and configured. In both cases, the members of the blob (pointer, size, flag) are defined for later use when the block is copied. Note that for both cases, size defines the size of the data that will be copied, either in-place or to shared memory. When writeBlob() completes, the target data buffer is defined in blob and values written to the parcel describing how the image data block is stored (in-place or shared memory) and, for shared memory, the attributes of the ashmem region.
android::Parcel::WritableBlob blob;
android::status_t status = p->writeBlob(size, &blob);
if (status) {
doThrowRE(env, "Could not write bitmap to parcel blob.");
return JNI_FALSE;
}
With the target buffer for the block now setup, the data can be copied using the pointer in the blob. Note that size defines the amount of data copied. Also note that there is only one size. The same value is used for in-place and shared memory targets.
bitmap->lockPixels();
const void* pSrc = bitmap->getPixels();
if (pSrc == NULL) {
memset(blob.data(), 0, size);
} else {
memcpy(blob.data(), pSrc, size);
}
bitmap->unlockPixels();
blob.release();
return JNI_TRUE;
}
That completes the analysis of Bitmap_writeToParcel. It is now clear that while small (<40K) images are written in-place and larger images are written to shared memory, the size of the data written is the same for both cases. The easiest and most direct way to see what that size is, is to create a test case using an image <40K, so that it will be written in-place. The size of the resulting parcel then reveals the size of the image data.
A second method for determining what size is requires an understanding of SkBitmap::getSize(). This is the function used in the code analyzed above to get the size of the image block.
SkBitmap::getSize() is define at line 130 of this file. It is:
size_t getSize() const { return fHeight * fRowBytes; }
Two other functions in the same file relevant to this explanation are height(), defined at line 98:
int height() const { return fHeight; }
and rowBytes(), defined at line 101:
int rowBytes() const { return fRowBytes; }
We saw these functions used in Bitmap_writeToParcel when the attributes of a bitmap are written to a parcel:
p->writeInt32(bitmap->height());
p->writeInt32(bitmap->rowBytes());
With this understanding of these functions, we now see that by dumping the first few ints in a parcel, we can see the values of fHeight and fRowBytes, from which we can infer the value returned by getSize().
The second code snippet below does this and provides further confirmation that the size of the data written to the Parcel corresponds to the value returned by getByteCount().
Test #1
This test creates a bitmap smaller than 40K to produce in-place storage of the image data. The parcel data size is then examined to show that getByteCount() determines the size of the image data stored in the parcel.
The first few statements in the code below are to produce a Bitmap smaller than 40K.
The logcat output confirms that the size of the data written to the Parcel corresponds to the value returned by getByteCount().
byteCount=38400 allocatedByteCount=38400 parcelDataSize=38428
byteCount=7680 allocatedByteCount=38400 parcelDataSize=7708
Code that produced the output shown:
// Setup to get a mutable bitmap less than 40 Kbytes
String path = "someSmallImage.jpg";
Bitmap bm0 = BitmapFactory.decodeFile(path);
// Need it mutable to change height
Bitmap bm1 = bm0.copy(bm0.getConfig(), true);
// Chop it to get a size less than 40K
bm1.setHeight(bm1.getHeight() / 32);
// Now we have a BitMap with size < 40K for the test
Bitmap bm2 = bm1.copy(bm0.getConfig(), true);
// What's the parcel size?
Parcel p1 = Parcel.obtain();
bm2.writeToParcel(p1, 0);
// Expect byteCount and allocatedByteCount to be the same
Log.i("Demo", String.format("byteCount=%d allocatedByteCount=%d parcelDataSize=%d",
bm2.getByteCount(), bm2.getAllocationByteCount(), p1.dataSize()));
// Resize to make byteCount and allocatedByteCount different
bm2.setHeight(bm2.getHeight() / 4);
// What's the parcel size?
Parcel p2 = Parcel.obtain();
bm2.writeToParcel(p2, 0);
// Show that byteCount determines size of data written to parcel
Log.i("Demo", String.format("byteCount=%d allocatedByteCount=%d parcelDataSize=%d",
bm2.getByteCount(), bm2.getAllocationByteCount(), p2.dataSize()));
p1.recycle();
p2.recycle();
Test #2
This test stores a bitmap to a parcel, then dumps the first few ints to get the values from which image data size can be inferred.
The logcat output with comments added:
// Bitmap attributes
bc=12000000 abc=12000000 hgt=1500 wid=2000 rbyt=8000 dens=213
// Attributes after height change. byteCount changed, allocatedByteCount not.
bc=744000 abc=12000000 hgt=93 wid=2000 rbyt=8000 dens=213
// Dump of parcel data. Parcel data size is 48. Image too large for in-place.
pds=48 mut=1 ctyp=4 atyp=1 hgt=93 wid=2000 rbyt=8000 dens=213
// Show value of getSize() derived from parcel data. It equals getByteCount().
bitmap->getSize()= 744000 getByteCount()=744000
Code that produced this output:
String path = "someImage.jpg";
Bitmap bm0 = BitmapFactory.decodeFile(path);
// Need it mutable to change height
Bitmap bm = bm0.copy(bm0.getConfig(), true);
// For reference, and to provide confidence that the parcel data dump is
// correct, log the bitmap attributes.
Log.i("Demo", String.format("bc=%d abc=%d hgt=%d wid=%d rbyt=%d dens=%d",
bm.getByteCount(), bm.getAllocationByteCount(),
bm.getHeight(), bm.getWidth(), bm.getRowBytes(), bm.getDensity()));
// Change size
bm.setHeight(bm.getHeight() / 16);
Log.i("Demo", String.format("bc=%d abc=%d hgt=%d wid=%d rbyt=%d dens=%d",
bm.getByteCount(), bm.getAllocationByteCount(),
bm.getHeight(), bm.getWidth(), bm.getRowBytes(), bm.getDensity()));
// Get a parcel and write the bitmap to it.
Parcel p = Parcel.obtain();
bm.writeToParcel(p, 0);
// When the image is too large to be written in-place,
// the parcel data size will be ~48 bytes (when there is no color map).
int parcelSize = p.dataSize();
// What are the first few ints in the parcel?
p.setDataPosition(0);
int mutable = p.readInt(); //1
int colorType = p.readInt(); //2
int alphaType = p.readInt(); //3
int width = p.readInt(); //4
int height = p.readInt(); //5 bitmap->height()
int rowBytes = p.readInt(); //6 bitmap->rowBytes()
int density = p.readInt(); //7
Log.i("Demo", String.format("pds=%d mut=%d ctyp=%d atyp=%d hgt=%d wid=%d rbyt=%d dens=%d",
parcelSize, mutable, colorType, alphaType, height, width, rowBytes, density));
// From code analysis, we know that the value returned
// by SkBitmap::getSize() is the size of the image data written.
// We also know that the value of getSize() is height()*rowBytes().
// These are the values in ints 5 and 6.
int imageSize = height * rowBytes;
// Show that the size of image data stored is Bitmap.getByteCount()
Log.i("Demo", String.format("bitmap->getSize()= %d getByteCount()=%d", imageSize, bm.getByteCount()));
p.recycle();
I am working on an image processing project. I have a SurfaceView where I want to show "images" form the jni side.
I followed this blog , the NDK ANativeWindow API instructions to get the pointer of the buffer and updated from the C side.
I got the code to run but my SurfaceView is not updating (not showing any image...). Also the callback surfaceChanged is not called when the buffer is updated.
Here is what I am doing:
JNI SIDE :
/*
* Class: com_example_myLib
* Method: renderImage
* Signature: (JI)V
*/
JNIEXPORT void JNICALL com_example_myLib_renderImage
(JNIEnv *mJNIEnv, jobject mjobject, jobject javaSurface) {
#ifdef DEBUG_I
LOGI("renderImage attempt !");
#endif
// load an ipl image. code tested and works well with ImageView.
IplImage *iplimage = loadMyIplImage();
int iplImageWidth = iplimage->width;
int iplImageHeitgh = iplimage->height;
char * javalBmpPointer = malloc(iplimage->width * iplimage->height * 4);
int _javaBmpRowBytes = iplimage->width * 4;
// code tested and works well with ImageView.
copyIplImageToARGBPointer(iplimage, javalBmpPointer, _javaBmpRowBytes,
iplimage->width, iplimage->height);
#ifdef DEBUG_I
LOGI("ANativeWindow_fromSurface");
#endif
ANativeWindow* window = ANativeWindow_fromSurface(env, javaSurface);
#ifdef DEBUG_I
LOGI("Got window %p", window);
#endif
if(window != 0 ){
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(window, &buffer, NULL) == 0) {
#ifdef DEBUG_I
LOGI("GANativeWindow_lock %p", buffer);
#endif
memcpy(buffer.bits, javalBmpPointer, iplimage->width* iplimage->height* 4); // ARGB_8888
ANativeWindow_unlockAndPost(window);
}
ANativeWindow_release(window);
}
}
java SIDE :
// every time that I want to reload the image:
renderImage(mySurfaceView.getHolder().getSurface());
Thanks for your time and help!
One of the most common problems leading to a blank SurfaceView is setting a background for the View. The View contents are intended to be a transparent "hole", used only for layout. The Surface contents are on a separate layer, below the View layer, so they are only visible if the View remains transparent.
The View background should generally be disabled, and nothing drawn on the View, unless you want some sort of "mask" effect (like rounded corners).
When i searched for how to find the size of an image before saving it on the SD card, i found this:
bitmap.getByteCount();
but that method is added in API 12 and i am using API 10. So again i found out this:
getByteCount() is just a convenience method which does exactly what you have placed in the else-block. In other words, if you simply rewrite getSizeInBytes to always return "bitmap.getRowBytes() * bitmap.getHeight()"
here:
Where the heck is Bitmap getByteCount()?
so, by calculating this bitmap.getRowBytes() * bitmap.getHeight() i got the value 120000 (117 KB).
where as the image size on the SD card is 1.6 KB.
What am i missing? or doing wrong?
Thank You
You are doing it correctly!
A quick way to know for sure if the values are valid, is to log it like this:
int numBytesByRow = bitmap.getRowBytes() * bitmap.getHeight();
int numBytesByCount = bitmap.getByteCount();
Log.v( TAG, "numBytesByRow=" + numBytesByRow );
Log.v( TAG, "numBytesByCount=" + numBytesByCount );
This gives the result:
03-29 17:31:10.493: V/ImageCache(19704): numBytesByRow=270000
03-29 17:31:10.493: V/ImageCache(19704): numBytesByCount=270000
So both are calculating the same number, which I suspect is the in-memory size of the bitmap. This is different than a JPG or PNG on disk as it is completely uncompressed.
For more info, we can look to AOSP and the source in the example project. This is the file used in the example project BitmapFun in the Android developer docs Caching Bitmaps
AOSP ImageCache.java
/**
* Get the size in bytes of a bitmap in a BitmapDrawable.
* #param value
* #return size in bytes
*/
#TargetApi(12)
public static int getBitmapSize(BitmapDrawable value) {
Bitmap bitmap = value.getBitmap();
if (APIUtil.hasHoneycombMR1()) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
As you can see this is the same technique they use
bitmap.getRowBytes() * bitmap.getHeight();
References:
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
http://code.google.com/p/adamkoch/source/browse/bitmapfun/
For now i am using this:
ByteArrayOutputStream bao = new ByteArrayOutputStream();
my_bitmap.compress(Bitmap.CompressFormat.PNG, 100, bao);
byte[] ba = bao.toByteArray();
int size = ba.length;
to get total no.of bytes as size. Because the value i get here perfectly matches the size(in bytes) on the image on SD card.
Nothing is missing! Your codesnippet shows exact the implementation from Android-Source:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/graphics/Bitmap.java#Bitmap.getByteCount%28%29
I think the differences in size are the result of image-compressing (jpg and so on).
Here is an alternative way:
public static int getBitmapByteCount(Bitmap bitmap) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1)
return bitmap.getRowBytes() * bitmap.getHeight();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
return bitmap.getByteCount();
return bitmap.getAllocationByteCount();
}
A statement from the IDE for getAllocationByteCount():
This can be larger than the result of getByteCount() if a bitmap is
reused to decode other bitmaps of smaller size, or by manual
reconfiguration. See reconfigure(int, int, Bitmap.Config),
setWidth(int), setHeight(int), setConfig(Bitmap.Config), and
BitmapFactory.Options.inBitmap. If a bitmap is not modified in this
way, this value will be the same as that returned by getByteCount().
may u can try this code
int pixels = bitmap.getHeight() * bitmap.getWidth();
int bytesPerPixel = 0;
switch(bitmap.getConfig()) {
case ARGB_8888:
bytesPerPixel = 4;
break;
case RGB_565:
bytesPerPixel = 2;
break;
case ARGB_4444:
bytesPerPixel = 2;
break;
case ALPHA_8 :
bytesPerPixel = 1;
break;
}
int byteCount = pixels / bytesPerPixel;
the image on the sd card has a different size because it's compressed. on the device it will depend on the width/height
Why don't you try dividing it between 1024? To get the KB instead of Bytes.
I try to read an image from sdcard (in emulator) and then create a Bitmap image with the
BitmapFactory.decodeByteArray
method. I set the options:
options.inPrefferedConfig = Bitmap.Config.ARGB_8888
options.inDither = false
Then I extract the pixels into a ByteBuffer.
ByteBuffer buffer = ByteBuffer.allocateDirect(width*height*4)
bitmap.copyPixelsToBuffer(buffer)
I use this ByteBuffer then in the JNI to convert it into RGB format and want to calculate on it.
But always I get false data - I test without modifying the ByteBuffer. Only thing I do is to put it into the native method into JNI. Then cast it into a unsigned char* and convert it back into a ByteBuffer before returning it back to Java.
unsigned char* buffer = (unsinged char*)(env->GetDirectBufferAddress(byteBuffer))
jobject returnByteBuffer = env->NewDirectByteBuffer(buffer, length)
Before displaying the image I get data back with
bitmap.copyPixelsFromBuffer( buffer )
But then it has wrong data in it.
My Question is if this is because the image is internally converted into RGB 565 or what is wrong here?
.....
Have an answer for it:
->>> yes, it is converted internally to RGB565.
Does anybody know how to create such an bitmap image from PNG with ARGB8888 pixel format?
If anybody has an idea, it would be great!
An ARGB_8888 Bitmap (on pre Honeycomb versions) is natively stored in the RGBA format.
So the alpha channel is moved at the end. You should take this into account when accessing a Bitmap's pixels natively.
I assume you are writing code for a version of Android lower than 3.2 (API level < 12), because since then the behavior of the methods
BitmapFactory.decodeFile(pathToImage);
BitmapFactory.decodeFile(pathToImage, opt);
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
has changed.
On older platforms (API level < 12) the BitmapFactory.decodeFile(..) methods try to return a Bitmap with RGB_565 config by default, if they can't find any alpha, which lowers the quality of an iamge. This is still ok, because you can enforce an ARGB_8888 bitmap using
options.inPrefferedConfig = Bitmap.Config.ARGB_8888
options.inDither = false
The real problem comes when each pixel of your image has an alpha value of 255 (i.e. completely opaque). In that case the Bitmap's flag 'hasAlpha' is set to false, even though your Bitmap has ARGB_8888 config. If your *.png-file had at least one real transparent pixel, this flag would have been set to true and you wouldn't have to worry about anything.
So when you want to create a scaled Bitmap using
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
the method checks whether the 'hasAlpha' flag is set to true or false, and in your case it is set to false, which results in obtaining a scaled Bitmap, which was automatically converted to the RGB_565 format.
Therefore on API level >= 12 there is a public method called
public void setHasAlpha (boolean hasAlpha);
which would have solved this issue. So far this was just an explanation of the problem.
I did some research and noticed that the setHasAlpha method has existed for a long time and it's public, but has been hidden (#hide annotation). Here is how it is defined on Android 2.3:
/**
* Tell the bitmap if all of the pixels are known to be opaque (false)
* or if some of the pixels may contain non-opaque alpha values (true).
* Note, for some configs (e.g. RGB_565) this call is ignore, since it does
* not support per-pixel alpha values.
*
* This is meant as a drawing hint, as in some cases a bitmap that is known
* to be opaque can take a faster drawing case than one that may have
* non-opaque per-pixel alpha values.
*
* #hide
*/
public void setHasAlpha(boolean hasAlpha) {
nativeSetHasAlpha(mNativeBitmap, hasAlpha);
}
Now here is my solution proposal. It does not involve any copying of bitmap data:
Checked at runtime using java.lang.Reflect if the current
Bitmap implementation has a public 'setHasAplha' method.
(According to my tests it works perfectly since API level 3, and i haven't tested lower versions, because JNI wouldn't work). You may have problems if a manufacturer has explicitly made it private, protected or deleted it.
Call the 'setHasAlpha' method for a given Bitmap object using JNI.
This works perfectly, even for private methods or fields. It is official that JNI does not check whether you are violating the access control rules or not.
Source: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9)
This gives us great power, which should be used wisely. I wouldn't try modifying a final field, even if it would work (just to give an example). And please note this is just a workaround...
Here is my implementation of all necessary methods:
JAVA PART:
// NOTE: this cannot be used in switch statements
private static final boolean SETHASALPHA_EXISTS = setHasAlphaExists();
private static boolean setHasAlphaExists() {
// get all puplic Methods of the class Bitmap
java.lang.reflect.Method[] methods = Bitmap.class.getMethods();
// search for a method called 'setHasAlpha'
for(int i=0; i<methods.length; i++) {
if(methods[i].getName().contains("setHasAlpha")) {
Log.i(TAG, "method setHasAlpha was found");
return true;
}
}
Log.i(TAG, "couldn't find method setHasAlpha");
return false;
}
private static void setHasAlpha(Bitmap bitmap, boolean value) {
if(bitmap.hasAlpha() == value) {
Log.i(TAG, "bitmap.hasAlpha() == value -> do nothing");
return;
}
if(!SETHASALPHA_EXISTS) { // if we can't find it then API level MUST be lower than 12
// couldn't find the setHasAlpha-method
// <-- provide alternative here...
return;
}
// using android.os.Build.VERSION.SDK to support API level 3 and above
// use android.os.Build.VERSION.SDK_INT to support API level 4 and above
if(Integer.valueOf(android.os.Build.VERSION.SDK) <= 11) {
Log.i(TAG, "BEFORE: bitmap.hasAlpha() == " + bitmap.hasAlpha());
Log.i(TAG, "trying to set hasAplha to true");
int result = setHasAlphaNative(bitmap, value);
Log.i(TAG, "AFTER: bitmap.hasAlpha() == " + bitmap.hasAlpha());
if(result == -1) {
Log.e(TAG, "Unable to access bitmap."); // usually due to a bug in the own code
return;
}
} else { //API level >= 12
bitmap.setHasAlpha(true);
}
}
/**
* Decodes a Bitmap from the SD card
* and scales it if necessary
*/
public Bitmap decodeBitmapFromFile(String pathToImage, int pixels_limit) {
Bitmap bitmap;
Options opt = new Options();
opt.inDither = false; //important
opt.inPreferredConfig = Bitmap.Config.ARGB_8888;
bitmap = BitmapFactory.decodeFile(pathToImage, opt);
if(bitmap == null) {
Log.e(TAG, "unable to decode bitmap");
return null;
}
setHasAlpha(bitmap, true); // if necessary
int numOfPixels = bitmap.getWidth() * bitmap.getHeight();
if(numOfPixels > pixels_limit) { //image needs to be scaled down
// ensures that the scaled image uses the maximum of the pixel_limit while keeping the original aspect ratio
// i use: private static final int pixels_limit = 1280*960; //1,3 Megapixel
imageScaleFactor = Math.sqrt((double) pixels_limit / (double) numOfPixels);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap,
(int) (imageScaleFactor * bitmap.getWidth()), (int) (imageScaleFactor * bitmap.getHeight()), false);
bitmap.recycle();
bitmap = scaledBitmap;
Log.i(TAG, "scaled bitmap config: " + bitmap.getConfig().toString());
Log.i(TAG, "pixels_limit = " + pixels_limit);
Log.i(TAG, "scaled_numOfpixels = " + scaledBitmap.getWidth()*scaledBitmap.getHeight());
setHasAlpha(bitmap, true); // if necessary
}
return bitmap;
}
Load your lib and declare the native method:
static {
System.loadLibrary("bitmaputils");
}
private static native int setHasAlphaNative(Bitmap bitmap, boolean value);
Native section ('jni' folder)
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := bitmaputils
LOCAL_SRC_FILES := bitmap_utils.c
LOCAL_LDLIBS := -llog -ljnigraphics -lz -ldl -lgcc
include $(BUILD_SHARED_LIBRARY)
bitmapUtils.c:
#include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#define LOG_TAG "BitmapTest"
#define Log_i(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define Log_e(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
// caching class and method IDs for a faster subsequent access
static jclass bitmap_class = 0;
static jmethodID setHasAlphaMethodID = 0;
jint Java_com_example_bitmaptest_MainActivity_setHasAlphaNative(JNIEnv * env, jclass clazz, jobject bitmap, jboolean value) {
AndroidBitmapInfo info;
void* pixels;
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
Log_e("Failed to get Bitmap info");
return -1;
}
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
Log_e("Incompatible Bitmap format");
return -1;
}
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
Log_e("Failed to lock the pixels of the Bitmap");
return -1;
}
// get class
if(bitmap_class == NULL) { //initializing jclass
// NOTE: The class Bitmap exists since API level 1, so it just must be found.
bitmap_class = (*env)->GetObjectClass(env, bitmap);
if(bitmap_class == NULL) {
Log_e("bitmap_class == NULL");
return -2;
}
}
// get methodID
if(setHasAlphaMethodID == NULL) { //initializing jmethodID
// NOTE: If this fails, because the method could not be found the App will crash.
// But we only call this part of the code if the method was found using java.lang.Reflect
setHasAlphaMethodID = (*env)->GetMethodID(env, bitmap_class, "setHasAlpha", "(Z)V");
if(setHasAlphaMethodID == NULL) {
Log_e("methodID == NULL");
return -2;
}
}
// call java instance method
(*env)->CallVoidMethod(env, bitmap, setHasAlphaMethodID, value);
// if an exception was thrown we could handle it here
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
Log_e("calling setHasAlpha threw an exception");
return -2;
}
if(AndroidBitmap_unlockPixels(env, bitmap) < 0) {
Log_e("Failed to unlock the pixels of the Bitmap");
return -1;
}
return 0; // success
}
That's it. We are done. I've posted the whole code for copy-and-paste purposes.
The actual code isn't that big, but making all these paranoid error checks makes it a lot bigger. I hope this could be helpful to anyone.