blob: 7dd6caad9ddee5492b8584e72ec091a243858fd4 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.graphics.cts;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorSpace;
import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.os.Parcel;
import android.util.Log;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.RequiresDevice;
import androidx.test.filters.SmallTest;
import com.android.compatibility.common.util.ColorUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.util.Arrays;
import junitparams.JUnitParamsRunner;
import junitparams.Parameters;
@SmallTest
@RunWith(JUnitParamsRunner.class)
public class BitmapColorSpaceTest {
private static final String LOG_TAG = "BitmapColorSpaceTest";
private Resources mResources;
@Before
public void setup() {
mResources = InstrumentationRegistry.getTargetContext().getResources();
}
@SuppressWarnings("deprecation")
@Test
public void createWithColorSpace() {
// We don't test HARDWARE configs because they are not compatible with mutable bitmaps
Bitmap.Config[] configs = new Bitmap.Config[] {
Bitmap.Config.ARGB_8888,
Bitmap.Config.RGB_565,
Bitmap.Config.ARGB_4444,
Bitmap.Config.RGBA_F16,
Bitmap.Config.RGBA_1010102,
};
// in most cases, createBitmap respects the ColorSpace
for (Bitmap.Config config : configs) {
for (ColorSpace.Named e : new ColorSpace.Named[] {
ColorSpace.Named.PRO_PHOTO_RGB,
ColorSpace.Named.ADOBE_RGB,
ColorSpace.Named.DISPLAY_P3,
ColorSpace.Named.DCI_P3,
ColorSpace.Named.BT709,
ColorSpace.Named.BT2020,
}) {
ColorSpace requested = ColorSpace.get(e);
Bitmap b = Bitmap.createBitmap(32, 32, config, false, requested);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(requested, cs);
}
// SRGB and LINEAR_SRGB are special.
ColorSpace sRGB = ColorSpace.get(ColorSpace.Named.SRGB);
ColorSpace extendedSrgb = ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB);
for (ColorSpace requested : new ColorSpace[] {
sRGB,
extendedSrgb,
}) {
Bitmap b = Bitmap.createBitmap(32, 32, config, false, requested);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
if (config == Bitmap.Config.RGBA_F16) {
assertSame(extendedSrgb, cs);
} else {
assertSame(sRGB, cs);
}
}
ColorSpace linearRgb = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB);
ColorSpace linearExtendedSrgb = ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
for (ColorSpace requested : new ColorSpace[] {
linearRgb,
linearExtendedSrgb,
}) {
Bitmap b = Bitmap.createBitmap(32, 32, config, false, requested);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
if (config == Bitmap.Config.RGBA_F16) {
assertSame(linearExtendedSrgb, cs);
} else {
assertSame(linearRgb, cs);
}
}
}
}
@Test
public void createAlpha8ColorSpace() {
Bitmap bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ALPHA_8);
assertNull(bitmap.getColorSpace());
for (ColorSpace cs : BitmapTest.getRgbColorSpaces()) {
bitmap = Bitmap.createBitmap(32, 32, Bitmap.Config.ALPHA_8, true, cs);
assertNull(bitmap.getColorSpace());
}
}
@Test
public void createDefaultColorSpace() {
ColorSpace sRGB = ColorSpace.get(ColorSpace.Named.SRGB);
Bitmap.Config[] configs = new Bitmap.Config[] {
Bitmap.Config.RGB_565, Bitmap.Config.ARGB_8888
};
for (Bitmap.Config config : configs) {
Bitmap bitmap = Bitmap.createBitmap(32, 32, config, true);
assertSame(sRGB, bitmap.getColorSpace());
}
}
@Test(expected = IllegalArgumentException.class)
public void createWithoutColorSpace() {
Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888, true, null);
}
@Test(expected = IllegalArgumentException.class)
public void createWithNonRgbColorSpace() {
Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888, true,
ColorSpace.get(ColorSpace.Named.CIE_LAB));
}
@Test(expected = IllegalArgumentException.class)
public void createWithNoTransferParameters() {
Bitmap.createBitmap(32, 32, Bitmap.Config.ARGB_8888, true,
new ColorSpace.Rgb("NoTransferParams",
new float[]{ 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f },
ColorSpace.ILLUMINANT_D50,
x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f),
0, 1));
}
@Test
public void createFromSourceWithColorSpace() {
for (ColorSpace rgb : BitmapTest.getRgbColorSpaces()) {
Bitmap.Config[] configs = new Bitmap.Config[] {
Bitmap.Config.ARGB_8888,
Bitmap.Config.RGB_565,
Bitmap.Config.ALPHA_8,
Bitmap.Config.ARGB_4444,
Bitmap.Config.RGBA_F16,
Bitmap.Config.RGBA_1010102,
};
for (Bitmap.Config config : configs) {
Bitmap orig = Bitmap.createBitmap(32, 32, config, false, rgb);
Bitmap cropped = Bitmap.createBitmap(orig, 0, 0, orig.getWidth() / 2,
orig.getHeight() / 2, null, false);
assertSame(orig.getColorSpace(), cropped.getColorSpace());
if (config == Bitmap.Config.ALPHA_8) {
assertNull(cropped.getColorSpace());
}
Matrix m = new Matrix();
m.setRotate(45, orig.getWidth() / 2, orig.getHeight() / 2);
Bitmap rotated = Bitmap.createBitmap(orig, 0, 0, orig.getWidth(),
orig.getHeight(), m, false);
switch (config) {
case ALPHA_8:
assertSame(Bitmap.Config.ARGB_8888, rotated.getConfig());
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), rotated.getColorSpace());
break;
case RGB_565:
assertSame(Bitmap.Config.ARGB_8888, rotated.getConfig());
// Fallthrough.
default:
assertSame("Mismatch with Config " + config,
orig.getColorSpace(), rotated.getColorSpace());
break;
}
}
}
}
@Test
public void sRGB() {
Bitmap b = BitmapFactory.decodeResource(mResources, R.drawable.robot);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
}
@Test
public void p3() {
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
Bitmap b = BitmapFactory.decodeStream(in);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void extendedSRGB() {
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void linearSRGB() {
String assetInLinearSRGB = "grayscale-linearSrgb.png";
try (InputStream in = mResources.getAssets().open(assetInLinearSRGB)) {
Bitmap b = BitmapFactory.decodeStream(in);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), cs);
} catch (IOException e) {
fail();
}
try (InputStream in = mResources.getAssets().open(assetInLinearSRGB)) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGBA_F16;
Bitmap b = BitmapFactory.decodeStream(in, null, options);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), cs);
} catch (IOException e) {
fail();
}
}
private static class Asset {
public final String name;
public final ColorSpace colorSpace;
Asset(String name, ColorSpace.Named e) {
this.name = name;
this.colorSpace = ColorSpace.get(e);
}
};
@Test
public void reconfigure() {
Asset[] assets = new Asset[] {
new Asset("green-p3.png", ColorSpace.Named.DISPLAY_P3),
new Asset("red-adobergb.png", ColorSpace.Named.ADOBE_RGB),
};
for (Asset asset : assets) {
for (Bitmap.Config config : new Bitmap.Config[] {
Bitmap.Config.ARGB_8888,
Bitmap.Config.RGB_565,
}) {
try (InputStream in = mResources.getAssets().open(asset.name)) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inMutable = true;
opts.inPreferredConfig = config;
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(asset.colorSpace, cs);
b.reconfigure(b.getWidth() / 4, b.getHeight() / 4, Bitmap.Config.RGBA_F16);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(asset.colorSpace, cs);
b.reconfigure(b.getWidth(), b.getHeight(), config);
cs = b.getColorSpace();
assertNotNull(cs);
assertSame(asset.colorSpace, cs);
} catch (IOException e) {
fail();
}
}
}
}
@Test
public void reuse() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inMutable = true;
Bitmap bitmap1 = null;
try (InputStream in = mResources.getAssets().open("green-srgb.png")) {
bitmap1 = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = bitmap1.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
} catch (IOException e) {
fail();
}
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
opts.inBitmap = bitmap1;
Bitmap bitmap2 = BitmapFactory.decodeStream(in, null, opts);
assertSame(bitmap1, bitmap2);
ColorSpace cs = bitmap2.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void getPixel() {
verifyGetPixel("green-p3.png", 0x75fb4cff);
verifyGetPixel("translucent-green-p3.png", 0x3a7d267f); // 50% translucent
}
private void verifyGetPixel(@NonNull String fileName, @ColorInt int rawColor) {
try (InputStream in = mResources.getAssets().open(fileName)) {
Bitmap b = BitmapFactory.decodeStream(in);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
verifyGetPixel(b, rawColor);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
verifyGetPixel(b, rawColor);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
verifyGetPixel(b, rawColor);
} catch (IOException e) {
fail();
}
}
private static void verifyGetPixel(@NonNull Bitmap b, @ColorInt int rawColor) {
ByteBuffer dst = ByteBuffer.allocate(b.getByteCount());
b.copyPixelsToBuffer(dst);
dst.rewind();
// Stored as RGBA
assertEquals(rawColor, dst.asIntBuffer().get());
int srgbColor = convertPremulColorToColorInt(rawColor, b.getColorSpace());
int srgb = b.getPixel(15, 15);
almostEqual(srgbColor, srgb, 3, 15 * b.getWidth() + 15);
}
private static int convertPremulColorToColorInt(int premulColor, ColorSpace premulCS) {
float alpha = (premulColor & 0xff) / 255.0f;
return Color.toArgb(Color.convert((premulColor >>> 24) / 255.0f / alpha,
((premulColor >> 16) & 0xff) / 255.0f / alpha,
((premulColor >> 8) & 0xff) / 255.0f / alpha,
alpha, premulCS, ColorSpace.get(ColorSpace.Named.SRGB)));
}
@Test
public void getPixels() {
verifyGetPixels("green-p3.png");
verifyGetPixels("translucent-green-p3.png"); // 50% translucent
}
private void verifyGetPixels(@NonNull String fileName) {
try (InputStream in = mResources.getAssets().open(fileName)) {
Bitmap b = BitmapFactory.decodeStream(in);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
ByteBuffer dst = ByteBuffer.allocate(b.getByteCount());
b.copyPixelsToBuffer(dst);
dst.rewind();
// Stored as RGBA
int expected = convertPremulColorToColorInt(dst.asIntBuffer().get(), b.getColorSpace());
verifyGetPixels(b, expected);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
verifyGetPixels(b, expected);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
verifyGetPixels(b, expected);
} catch (IOException e) {
fail();
}
}
private static void verifyGetPixels(@NonNull Bitmap b, @ColorInt int expected) {
int[] pixels = new int[b.getWidth() * b.getHeight()];
b.getPixels(pixels, 0, b.getWidth(), 0, 0, b.getWidth(), b.getHeight());
for (int i = 0; i < pixels.length; i++) {
int pixel = pixels[i];
almostEqual(expected, pixel, 3, i);
}
}
@Test
public void setPixel() {
verifySetPixel("green-p3.png", 0xffff0000, 0xea3323ff);
verifySetPixel("translucent-green-p3.png", 0x7fff0000, 0x7519127f);
}
private void verifySetPixel(@NonNull String fileName,
@ColorInt int newColor, @ColorInt int expectedColor) {
try (InputStream in = mResources.getAssets().open(fileName)) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inMutable = true;
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
assertTrue(b.isMutable());
verifySetPixel(b, newColor, expectedColor);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
assertTrue(b.isMutable());
verifySetPixel(b, newColor, expectedColor);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
assertTrue(b.isMutable());
verifySetPixel(b, newColor, expectedColor);
} catch (IOException e) {
fail();
}
}
private static void verifySetPixel(@NonNull Bitmap b,
@ColorInt int newColor, @ColorInt int expectedColor) {
assertTrue(b.isMutable());
b.setPixel(0, 0, newColor);
ByteBuffer dst = ByteBuffer.allocate(b.getByteCount());
b.copyPixelsToBuffer(dst);
dst.rewind();
// Stored as RGBA
ColorUtils.verifyColor(expectedColor, dst.asIntBuffer().get(), 1);
}
@Test
public void setPixels() {
verifySetPixels("green-p3.png", 0xffff0000, 0xea3323ff);
verifySetPixels("translucent-green-p3.png", 0x7fff0000, 0x7519127f);
}
private void verifySetPixels(@NonNull String fileName,
@ColorInt int newColor, @ColorInt int expectedColor) {
try (InputStream in = mResources.getAssets().open(fileName)) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inMutable = true;
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
assertNotNull(b.getColorSpace());
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
verifySetPixels(b, newColor, expectedColor);
b = Bitmap.createBitmap(b, 0, 0, b.getWidth() / 2, b.getHeight() / 2);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
assertTrue(b.isMutable());
verifySetPixels(b, newColor, expectedColor);
b = Bitmap.createScaledBitmap(b, b.getWidth() / 2, b.getHeight() / 2, true);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
assertTrue(b.isMutable());
verifySetPixels(b, newColor, expectedColor);
} catch (IOException e) {
fail();
}
}
private static void verifySetPixels(@NonNull Bitmap b,
@ColorInt int newColor, @ColorInt int expectedColor) {
assertTrue(b.isMutable());
int[] pixels = new int[b.getWidth() * b.getHeight()];
Arrays.fill(pixels, newColor);
b.setPixels(pixels, 0, b.getWidth(), 0, 0, b.getWidth(), b.getHeight());
ByteBuffer dst = ByteBuffer.allocate(b.getByteCount());
b.copyPixelsToBuffer(dst);
dst.rewind();
IntBuffer buffer = dst.asIntBuffer();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < pixels.length; i++) {
// Stored as RGBA
ColorUtils.verifyColor(expectedColor, buffer.get(), 1);
}
}
@Test
public void writeColorSpace() {
verifyColorSpaceMarshalling("green-srgb.png", ColorSpace.get(ColorSpace.Named.SRGB));
verifyColorSpaceMarshalling("green-p3.png", ColorSpace.get(ColorSpace.Named.DISPLAY_P3));
verifyColorSpaceMarshalling("blue-16bit-srgb.png",
ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB));
Bitmap bitmapIn = BitmapFactory.decodeResource(mResources, R.drawable.robot);
verifyParcelUnparcel(bitmapIn, ColorSpace.get(ColorSpace.Named.SRGB));
}
private void verifyColorSpaceMarshalling(
@NonNull String fileName, @NonNull ColorSpace colorSpace) {
try (InputStream in = mResources.getAssets().open(fileName)) {
Bitmap bitmapIn = BitmapFactory.decodeStream(in);
verifyParcelUnparcel(bitmapIn, colorSpace);
} catch (IOException e) {
fail();
}
}
private void verifyParcelUnparcel(Bitmap bitmapIn, ColorSpace expected) {
ColorSpace cs = bitmapIn.getColorSpace();
assertNotNull(cs);
assertSame(expected, cs);
Parcel p = Parcel.obtain();
bitmapIn.writeToParcel(p, 0);
p.setDataPosition(0);
Bitmap bitmapOut = Bitmap.CREATOR.createFromParcel(p);
cs = bitmapOut.getColorSpace();
assertNotNull(cs);
assertSame(expected, cs);
p.recycle();
}
@Test
public void p3rgb565() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.RGB_565;
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void p3hardware() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.HARDWARE;
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void guessSRGB() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("green-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void guess16bitUntagged() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void guessProPhotoRGB() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("blue-16bit-prophoto.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void guessP3() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void guessAdobeRGB() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("red-adobergb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.ADOBE_RGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void guessUnknown() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("purple-displayprofile.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertEquals("Unknown", cs.getName());
} catch (IOException e) {
fail();
}
}
@Test
public void guessCMYK() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open("purple-cmyk.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = opts.outColorSpace;
assertNull(b);
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpaceP3ToSRGB() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
assertEquals(opts.inPreferredColorSpace, opts.outColorSpace);
verifyGetPixel(b, 0x2ff00ff);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpaceSRGBToP3() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
try (InputStream in = mResources.getAssets().open("green-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
assertEquals(opts.inPreferredColorSpace, opts.outColorSpace);
verifyGetPixel(b, 0x75fb4cff);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpaceWith16BitSrc() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB);
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.ADOBE_RGB), cs);
assertSame(opts.inPreferredColorSpace, opts.outColorSpace);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpaceWith16BitDst() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.RGBA_F16;
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpaceWith16BitSrcAndDst() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB);
opts.inPreferredConfig = Bitmap.Config.RGBA_F16;
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(opts.inPreferredColorSpace, cs);
assertSame(opts.inPreferredColorSpace, opts.outColorSpace);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpaceWith16BitWithDecreasedGamut() {
final String asset = "blue-16bit-prophoto.png";
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
try (InputStream in = mResources.getAssets().open(asset)) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
assertNull(b);
assertEquals(ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB), opts.outColorSpace);
assertEquals(Bitmap.Config.RGBA_F16, opts.outConfig);
} catch (IOException e) {
fail();
}
opts.inJustDecodeBounds = false;
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.DISPLAY_P3);
try (InputStream in = mResources.getAssets().open(asset)) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
}
@Test
public void inColorSpace565() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.ADOBE_RGB);
opts.inPreferredConfig = Bitmap.Config.RGB_565;
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
Bitmap b = BitmapFactory.decodeStream(in, null, opts);
ColorSpace cs = b.getColorSpace();
assertNotNull(cs);
assertSame(opts.inPreferredColorSpace, cs);
assertSame(opts.inPreferredColorSpace, opts.outColorSpace);
} catch (IOException e) {
fail();
}
}
@Test(expected = IllegalArgumentException.class)
public void inColorSpaceNotRGB() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.CIE_LAB);
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
BitmapFactory.decodeStream(in, null, opts);
} catch (IOException e) {
fail();
}
}
@Test(expected = IllegalArgumentException.class)
public void inColorSpaceNoTransferParameters() {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredColorSpace = new ColorSpace.Rgb("NoTransferParams",
new float[]{ 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f },
ColorSpace.ILLUMINANT_D50,
x -> Math.pow(x, 1.0f / 2.2f), x -> Math.pow(x, 2.2f),
0, 1);
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
BitmapFactory.decodeStream(in, null, opts);
} catch (IOException e) {
fail();
}
}
@Test
public void copyF16() {
// Copying from (LINEAR_)SRGB to RGBA_F16 results in (LINEAR_)EXTENDED_SRGB.
ColorSpace[] srcCS = new ColorSpace[] { ColorSpace.get(ColorSpace.Named.SRGB),
ColorSpace.get(ColorSpace.Named.LINEAR_SRGB) };
ColorSpace[] dstCS = new ColorSpace[] { ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB),
ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB) };
for (int i = 0; i < srcCS.length; ++i) {
for (Bitmap.Config config : new Bitmap.Config[] { Bitmap.Config.ARGB_8888,
Bitmap.Config.RGB_565 }) {
Bitmap b = Bitmap.createBitmap(10, 10, config, false, srcCS[i]);
assertSame(srcCS[i], b.getColorSpace());
for (boolean mutable : new boolean[] { true, false }) {
Bitmap copy = b.copy(Bitmap.Config.RGBA_F16, mutable);
assertSame(dstCS[i], copy.getColorSpace());
}
}
}
// The same is true for the reverse
for (int i = 0; i < srcCS.length; ++i) {
Bitmap b = Bitmap.createBitmap(10, 10, Bitmap.Config.RGBA_F16, false, dstCS[i]);
assertSame(dstCS[i], b.getColorSpace());
for (Bitmap.Config config : new Bitmap.Config[] { Bitmap.Config.ARGB_8888,
Bitmap.Config.RGB_565 }) {
for (boolean mutable : new boolean[] { true, false }) {
Bitmap copy = b.copy(config, mutable);
assertSame(srcCS[i], copy.getColorSpace());
}
}
}
}
@Test
public void copyAlpha8() {
for (Bitmap.Config srcConfig : new Bitmap.Config[] {
Bitmap.Config.ALPHA_8,
Bitmap.Config.RGB_565,
Bitmap.Config.ARGB_8888,
Bitmap.Config.RGBA_F16,
}) {
Bitmap b = Bitmap.createBitmap(1, 1, srcConfig);
assertNotNull(b);
if (srcConfig == Bitmap.Config.ALPHA_8) {
assertNull(b.getColorSpace());
} else {
assertNotNull(b.getColorSpace());
}
Bitmap copy = b.copy(Bitmap.Config.ALPHA_8, false);
assertNotNull(copy);
assertNull(copy.getColorSpace());
Bitmap copy2 = copy.copy(srcConfig, false);
switch (srcConfig) {
case RGBA_F16:
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB),
copy2.getColorSpace());
break;
case ALPHA_8:
assertNull(b.getColorSpace());
break;
default:
assertSame("Copied from ALPHA_8 to " + srcConfig,
ColorSpace.get(ColorSpace.Named.SRGB), copy2.getColorSpace());
}
}
}
@Test
public void copyHardwareToAlpha8() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.HARDWARE;
Bitmap b = BitmapFactory.decodeResource(mResources, R.drawable.robot, options);
assertSame(Bitmap.Config.HARDWARE, b.getConfig());
assertNotNull(b.getColorSpace());
Bitmap copy = b.copy(Bitmap.Config.ALPHA_8, false);
assertNull(copy.getColorSpace());
}
@Test
public void copy() {
Bitmap b = BitmapFactory.decodeResource(mResources, R.drawable.robot);
Bitmap c;
ColorSpace cs;
boolean[] trueFalse = new boolean[] { true, false };
for (boolean mutable : trueFalse) {
c = b.copy(Bitmap.Config.ARGB_8888, mutable);
cs = c.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), cs);
}
try (InputStream in = mResources.getAssets().open("green-p3.png")) {
b = BitmapFactory.decodeStream(in);
c = b.copy(Bitmap.Config.ARGB_8888, false);
cs = c.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
c = b.copy(Bitmap.Config.ARGB_8888, true);
cs = c.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), cs);
} catch (IOException e) {
fail();
}
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
b = BitmapFactory.decodeStream(in);
c = b.copy(Bitmap.Config.RGBA_F16, false);
cs = c.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
c = b.copy(Bitmap.Config.RGBA_F16, true);
cs = c.getColorSpace();
assertNotNull(cs);
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), cs);
} catch (IOException e) {
fail();
}
}
@SuppressWarnings("SameParameterValue")
private static void almostEqual(@ColorInt int expected,
@ColorInt int pixel, int threshold, int index) {
int diffA = Math.abs((expected >>> 24) - (pixel >>> 24));
int diffR = Math.abs(((expected >> 16) & 0xff) - ((pixel >> 16) & 0xff));
int diffG = Math.abs(((expected >> 8) & 0xff) - ((pixel >> 8) & 0xff));
int diffB = Math.abs((expected & 0xff) - (pixel & 0xff));
boolean pass = diffA + diffR + diffG + diffB <= threshold;
if (!pass) {
Log.d(LOG_TAG, "Expected 0x" + Integer.toHexString(expected) +
" but was 0x" + Integer.toHexString(pixel) + " with index " + index);
}
assertTrue(pass);
}
private Object[] compressFormatsAndColorSpaces() {
return Utils.crossProduct(Bitmap.CompressFormat.values(),
BitmapTest.getRgbColorSpaces().toArray());
}
@Test
@Parameters(method = "compressFormatsAndColorSpaces")
public void testEncodeColorSpace(Bitmap.CompressFormat format, ColorSpace colorSpace) {
Bitmap b = null;
ColorSpace decodedColorSpace = null;
ImageDecoder.Source src = ImageDecoder.createSource(mResources.getAssets(),
"blue-16bit-srgb.png");
try {
b = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
decoder.setTargetColorSpace(colorSpace);
});
assertNotNull(b);
assertEquals(Bitmap.Config.RGBA_F16, b.getConfig());
decodedColorSpace = b.getColorSpace();
// Requesting a ColorSpace with an EXTENDED variant will use the EXTENDED one because
// the image is 16-bit.
if (colorSpace == ColorSpace.get(ColorSpace.Named.SRGB)) {
assertSame(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB), decodedColorSpace);
} else if (colorSpace == ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)) {
assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB),
decodedColorSpace);
} else {
assertSame(colorSpace, decodedColorSpace);
}
} catch (IOException e) {
fail("Failed with " + e);
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertTrue("Failed to encode F16 to " + format, b.compress(format, 100, out));
byte[] array = out.toByteArray();
src = ImageDecoder.createSource(ByteBuffer.wrap(array));
try {
Bitmap b2 = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
ColorSpace encodedColorSpace = b2.getColorSpace();
if (format == Bitmap.CompressFormat.PNG) {
assertEquals(Bitmap.Config.RGBA_F16, b2.getConfig());
assertSame(decodedColorSpace, encodedColorSpace);
} else {
// Compressing to the other formats does not support creating a compressed version
// that we will decode to F16.
assertEquals(Bitmap.Config.ARGB_8888, b2.getConfig());
// Decoding an EXTENDED variant to 8888 results in the non-extended variant.
if (decodedColorSpace == ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)) {
assertSame(ColorSpace.get(ColorSpace.Named.SRGB), encodedColorSpace);
} else if (decodedColorSpace
== ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB)) {
assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_SRGB), encodedColorSpace);
} else {
assertSame(decodedColorSpace, encodedColorSpace);
}
}
} catch (IOException e) {
fail("Failed with " + e);
}
}
@Test
public void testEncodeP3hardware() {
Bitmap b = null;
ImageDecoder.Source src = ImageDecoder.createSource(mResources.getAssets(),
"green-p3.png");
try {
b = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE);
});
assertNotNull(b);
assertEquals(Bitmap.Config.HARDWARE, b.getConfig());
assertEquals(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b.getColorSpace());
} catch (IOException e) {
fail("Failed with " + e);
}
for (Bitmap.CompressFormat format : Bitmap.CompressFormat.values()) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertTrue("Failed to encode 8888 to " + format, b.compress(format, 100, out));
byte[] array = out.toByteArray();
src = ImageDecoder.createSource(ByteBuffer.wrap(array));
try {
Bitmap b2 = ImageDecoder.decodeBitmap(src);
assertEquals("Wrong color space for " + format,
ColorSpace.get(ColorSpace.Named.DISPLAY_P3), b2.getColorSpace());
} catch (IOException e) {
fail("Failed with " + e);
}
}
}
@Test
@RequiresDevice // SwiftShader does not yet have support for F16 in HARDWARE b/75778024
public void test16bitHardware() {
// Decoding to HARDWARE may use EXTENDED_SRGB or SRGB, depending
// on whether F16 is supported in HARDWARE.
try (InputStream in = mResources.getAssets().open("blue-16bit-srgb.png")) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.HARDWARE;
Bitmap b = BitmapFactory.decodeStream(in, null, options);
assertEquals(Bitmap.Config.HARDWARE, b.getConfig());
final ColorSpace cs = b.getColorSpace();
if (cs != ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)
&& cs != ColorSpace.get(ColorSpace.Named.SRGB)) {
fail("Unexpected color space " + cs);
}
} catch (Exception e) {
fail("Failed with " + e);
}
}
@Test
public void testProPhoto() throws IOException {
ColorSpace extendedSrgb = ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB);
Color blue = Color.valueOf(0, 0, 1, 1, ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB));
Color expected = blue.convert(extendedSrgb);
try (InputStream in = mResources.getAssets().open("blue-16bit-prophoto.png")) {
Bitmap src = BitmapFactory.decodeStream(in, null, null);
Bitmap dst = Bitmap.createBitmap(src.getWidth(), src.getHeight(),
Bitmap.Config.RGBA_F16, true, extendedSrgb);
Canvas c = new Canvas(dst);
c.drawBitmap(src, 0, 0, null);
ColorUtils.verifyColor("PRO_PHOTO image did not convert properly", expected,
dst.getColor(0, 0), .001f);
}
}
@Test
public void testGrayscaleProfile() throws IOException {
ImageDecoder.Source source = ImageDecoder.createSource(mResources.getAssets(),
"gimp-d65-grayscale.jpg");
Bitmap bm = ImageDecoder.decodeBitmap(source, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
ColorSpace cs = bm.getColorSpace();
assertNotNull(cs);
assertTrue(cs instanceof ColorSpace.Rgb);
ColorSpace.Rgb rgbCs = (ColorSpace.Rgb) cs;
// A gray color space uses a special primaries array of all 1s.
float[] primaries = rgbCs.getPrimaries();
assertNotNull(primaries);
assertEquals(6, primaries.length);
for (float primary : primaries) {
assertEquals(0, Float.compare(primary, 1.0f));
}
// A gray color space will have all zeroes in the transform
// and inverse transform, except for the diagonal.
for (float[] transform : new float[][]{rgbCs.getTransform(), rgbCs.getInverseTransform()}) {
assertNotNull(transform);
assertEquals(9, transform.length);
for (int index : new int[] { 1, 2, 3, 5, 6, 7 }) {
assertEquals(0, Float.compare(0.0f, transform[index]));
}
}
// When creating another Bitmap with the same ColorSpace, the two
// ColorSpaces should be equal.
Bitmap otherBm = Bitmap.createBitmap(null, 100, 100, Bitmap.Config.ARGB_8888, true, cs);
assertEquals(cs, otherBm.getColorSpace());
// Same for a scaled bitmap.
Bitmap scaledBm = Bitmap.createScaledBitmap(bm, bm.getWidth() / 4, bm.getHeight() / 4,
true);
assertEquals(cs, scaledBm.getColorSpace());
// A previous ColorSpace bug resulted in a Bitmap created like scaledBm
// having all black pixels. Verify that the Bitmap contains colors other
// than black and white.
boolean foundOtherColor = false;
final int width = scaledBm.getWidth();
final int height = scaledBm.getHeight();
int[] pixels = new int[width * height];
scaledBm.getPixels(pixels, 0, width, 0, 0, width, height);
for (int pixel : pixels) {
if (pixel != Color.BLACK && pixel != Color.WHITE) {
foundOtherColor = true;
break;
}
}
assertTrue(foundOtherColor);
}
}