Search code examples
androidbitmapandroid-ndkalpha

Bitmap setPixels will lose alpha channel when background is black


I am trying to draw a custom view in Android, with canvas.drawBitmap() method. However, I found the alpha channel will be lost if I do this in native JNI code and the background is black. To summary, the case is:

  1. Call java bitmap.setPixels() and set bitmap pixels color in NDK when background is white, both bitmap display correctly
  2. Call java bitmap.setPixels() and set bitmap pixels color in NDK when background is black, only the bitmap drawn by java API displays correctly, the one drawn with NDK lost alpha channel

The question is why the result on white background is OK but not OK on black background? Am I missing anything or doing it in a wrong way?

Layout XML file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/black"
        android:orientation="horizontal"
        android:padding="16dp" >

        <com.example.android.TestView
            android:id="@+id/testview1"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />

        <com.example.android.TestView
            android:id="@+id/testview2"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:orientation="horizontal"
        android:padding="16dp" >

        <com.example.android.TestView
            android:id="@+id/testview3"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />

        <com.example.android.TestView
            android:id="@+id/testview4"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />
    </LinearLayout>

</LinearLayout>

MainActivity.java :

package com.example.android;
import com.example.android.R;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TestView tv2 = (TestView) findViewById(R.id.testview2);
        TestView tv4 = (TestView) findViewById(R.id.testview4);
        tv2.setDrawFromNative();
        tv4.setDrawFromNative();
    }
}

TestView.java :

package com.example.android;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

public class TestView extends View {
    private Bitmap mBitmap;
    private boolean mDrawFromNative;
    private static final int WIDTH = 320;
    private static final int HEIGHT = 320;

    static {
        System.loadLibrary("bitmaptest");
    }
    private native void nativeDrawBitmap(Object bitmap);

    private static void javaDrawBitmap(Bitmap bitmap) {
        int pixels[] = new int[WIDTH * HEIGHT];
        for (int i = 0; i < pixels.length; i++) {
            pixels[i] = 0x88FF0000;
        }
        bitmap.setPixels(pixels, 0, WIDTH, 0, 0, WIDTH, HEIGHT);
    }

    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
    }

    public void setDrawFromNative() {
        mDrawFromNative = true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(mDrawFromNative) {
            nativeDrawBitmap(mBitmap);
        } else {
            javaDrawBitmap(mBitmap);
        }
        canvas.drawBitmap(mBitmap, 0, 0, null);
    }
}

TestNative.cpp:

#include <jni.h>
#include <android/bitmap.h>
#include <android/Log.h>

#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define LOG_TAG "BMPTEST"

extern "C" {
void Java_com_example_android_TestView_nativeDrawBitmap(JNIEnv* env, jobject thiz, jobject bitmap) {

    AndroidBitmapInfo info;
    void* dst_pixels;
    int   ret;

    if((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &dst_pixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return;
    }

    unsigned int *dst = (unsigned int *)dst_pixels;
    for(int i=0; i< info.width * info.height; i++) {
            *(dst+i) = (0x88<<24 | 0xff | 0x00<<8 | 0x00<<16); //(ARGB->ABGR)
    }
    AndroidBitmap_unlockPixels(env, bitmap);
}
}

Android.mk for native code:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libbitmaptest
LOCAL_SRC_FILES := \
    TestNative.cpp
LOCAL_LDLIBS += -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)

ScreenShot of the result: ScreenShot of the result


Solution

  • Android stores bitmaps with pre-multiplied alpha. When you call setPixels() from Java, the RGB color values are automatically multiplied by the alpha values and stored in the bitmap. However, when you call Android_lockPixels() from native code and then write directly to the memory, you need to do the pre-multiplication yourself or else it's going to turn out wrong. If you change your code to:

     int premultipliedR = (0xff * 0x88) >> 8;
     for(int i=0; i< info.width * info.height; i++) {
            *(dst+i) = (0x88<<24 | premultipliedR | 0x00<<8 | 0x00<<16);
    

    then both Bitmaps should render the same.

    So why does it appear as if the bitmaps lose the alpha channel when the background is black but not for a white background? Well it turns out that's just a coincidence based on the numbers you've chosen.

    The basic alpha blending formula is:

     dest.r = ((dest.r * (256 - source.a)) + (source.r * source.a)) >> 8;
     dest.g = ((dest.g * (256 - source.a)) + (source.g * source.a)) >> 8;
     dest.b = ((dest.b * (256 - source.a)) + (source.b * source.a)) >> 8;
    

    where dest is the background pixel and source is the pixel in your bitmap. Pre-multiplying the alpha changes this to:

     dest.r = ((dest.r * (256 - source.a)) >> 8) + source.premultiplied_r;
     dest.g = ((dest.g * (256 - source.a)) >> 8) + source.premultiplied_g;
     dest.b = ((dest.b * (256 - source.a)) >> 8) + source.premultiplied_b;
    

    which saves a bunch of multiplies. The results are all clamped to 255. I'm not claiming this is the exact formula used, but it is something pretty close to it.

    Plugging the numbers in, for your Java bitmap, pre-multiplied r, g, b are going to be 0x87 (or 0x88 depending on how they do the rounding etc), 0x00 and 0x00. For your native bitmap, they are going to be 0xff, 0x00 and 0x00 because you didn't pre-multiply. Alpha-blending this with a black background is the same as just adding zero, since the dest. r, g, b values are all zero. So the results look different.

    In the case of a white background, dest.g and dest.b are going to end up the same in both cases, since the pre-multiplied g and b values are zero in both the Java and native bitmaps. In the case of dest.r, the result should be 255. In the case of the native bitmap, the value overflows because of the wrong value for pre-multiplied r, but it gets clamped to 255, so the results end up looking the same.

    In short, the pre-multipled r value is too high for your native bitmap, so you end up with a too-high r value in cases where the result should have been < 255. In cases where the result should have been 255, it doesn't matter if it's too high because it gets clamped at 255 anyway.