robolectric / robolectric

Android Unit Testing Framework
http://robolectric.org
Other
5.91k stars 1.37k forks source link

Native Bitmap copyPixelsToBuffer Broken for ARGB_8888 #8372

Open alonalbert opened 1 year ago

alonalbert commented 1 year ago

For a bitmap created with ARGB_8888, the method copyPixelsToBuffer behaves differently in Robolectric @GraphicsMode(NATIVE) that it does on a device.

Each pixel is represented by 4 bytes in the output buffer but on a device, the bytes are RGBA while in Robolectric, they are BGRA.

This can be demonstrated by the following test:

    @Test
    fun testBitmapColor() {
        val color = 0xff102030.toInt()
        val bitmap = Bitmap.createBitmap(listOf(color).toIntArray(), 1, 1, ARGB_8888)
        val buffer = ByteBuffer.allocate(bitmap.byteCount)
        bitmap.copyPixelsToBuffer(buffer)
        buffer.rewind()

        val bytes = (0 until buffer.limit()).joinToString(" ") { "%02x".format(buffer.get(it)) }

        assertEquals("10 20 30 ff", bytes)
    }

This test will succeed when run with @RunWith(AndroidJUnit4::class) but will fail when run with:

@RunWith(RobolectricTestRunner::class)
@GraphicsMode(NATIVE)

With:

expected:<[10 20 3]0 ff> but was:<[30 20 1]0 ff>
Expected :10 20 30 ff
Actual   :30 20 10 ff

Note that for RGB_565, both Robolectric and device emit the same results.

utzcoz commented 1 year ago

cc @hoisie and @JuliaSullivanGoogle for RNG's compatibility behavior.

MGaetan89 commented 4 months ago

@alonalbert can you try with Robolectric 4.13 to see if this issue still exists?

alonalbert commented 4 months ago

Now it fails with:

Expected :10 20 30 ff
Actual   :ff 10 20 30

I realize that ff 10 20 30 looks correct since we have the ARGB in that order, but when I run the test as an instrumented test, it does pass:

As a Robolectric test, it fails:

@RunWith(RobolectricTestRunner::class)
@GraphicsMode(NATIVE)
class ExampleUnitTest {
    @Test
    fun testBitmapColor() {
        val color = 0xff102030.toInt()
        val bitmap = Bitmap.createBitmap(listOf(color).toIntArray(), 1, 1, ARGB_8888)
        val buffer = ByteBuffer.allocate(bitmap.byteCount)
        bitmap.copyPixelsToBuffer(buffer)
        buffer.rewind()

        val bytes = (0 until buffer.limit()).joinToString(" ") { "%02x".format(buffer.get(it)) }

        assertEquals("10 20 30 ff", bytes)
    }
}

As an Instrumented test, it passes:

@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
    @Test
    fun testBitmapColor() {
        val color = 0xff102030.toInt()
        val bitmap = Bitmap.createBitmap(listOf(color).toIntArray(), 1, 1, ARGB_8888)
        val buffer = ByteBuffer.allocate(bitmap.byteCount)
        bitmap.copyPixelsToBuffer(buffer)
        buffer.rewind()

        val bytes = (0 until buffer.limit()).joinToString(" ") { "%02x".format(buffer.get(it)) }

        assertEquals("10 20 30 ff", bytes)
    }
}
hoisie commented 4 months ago

In Android when a Bitmap is created, the underlying Skia Bitmap (SkBitmap) object in native code is using kN32_SkColorType for the memory layout of the pixel data. This can vary across architectures depending on endianness. The value is determined by ifdef logic at build-time, so it depends on the build machine. See:

https://cs.android.com/android/platform/superproject/main/+/main:external/skia/include/core/SkColorType.h;l=57-62

I think on Linux and Windows, it ends up being kBGRA_8888_SkColorType, but on Mac it is kRGBA_8888_SkColorType.

@alonalbert if that test is run on Mac, my guess is that it would pass.

As far as I know, the best approach is to usebitmap.getPixels to return unpremultiplied ARGB, which is consistent across platforms. There may be something you can do with AWT's BufferedImage, but I'm not 100% sure.