blob: b7f9c6df9dec0f98944b0432511c82d891811e4e [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.assertFalse;
import static org.junit.Assert.assertNotEquals;
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.ContentResolver;
import android.content.Context;
import android.content.res.AssetManager;
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.ImageDecoder.DecodeException;
import android.graphics.ImageDecoder.OnPartialImageListener;
import android.graphics.PixelFormat;
import android.graphics.PostProcessor;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.NinePatchDrawable;
import android.net.Uri;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.DisplayMetrics;
import android.util.Size;
import android.util.TypedValue;
import androidx.core.content.FileProvider;
import com.android.compatibility.common.util.BitmapUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.function.IntFunction;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;
@RunWith(AndroidJUnit4.class)
public class ImageDecoderTest {
private Resources mRes;
private ContentResolver mContentResolver;
private static final class Record {
public final int resId;
public final int width;
public final int height;
public final String mimeType;
public final ColorSpace colorSpace;
Record(int resId, int width, int height, String mimeType, ColorSpace colorSpace) {
this.resId = resId;
this.width = width;
this.height = height;
this.mimeType = mimeType;
this.colorSpace = colorSpace;
}
}
private static final ColorSpace sSRGB = ColorSpace.get(ColorSpace.Named.SRGB);
private static final Record[] RECORDS = new Record[] {
new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", sSRGB),
new Record(R.drawable.png_test, 640, 480, "image/png", sSRGB),
new Record(R.drawable.gif_test, 320, 240, "image/gif", sSRGB),
new Record(R.drawable.bmp_test, 320, 240, "image/bmp", sSRGB),
new Record(R.drawable.webp_test, 640, 480, "image/webp", sSRGB),
new Record(R.drawable.google_chrome, 256, 256, "image/x-ico", sSRGB),
new Record(R.drawable.color_wheel, 128, 128, "image/x-ico", sSRGB),
new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", sSRGB),
};
// offset is how many bytes to offset the beginning of the image.
// extra is how many bytes to append at the end.
private byte[] getAsByteArray(int resId, int offset, int extra) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
writeToStream(output, resId, offset, extra);
return output.toByteArray();
}
private void writeToStream(OutputStream output, int resId, int offset, int extra) {
InputStream input = mRes.openRawResource(resId);
byte[] buffer = new byte[4096];
int bytesRead;
try {
for (int i = 0; i < offset; ++i) {
output.write(0);
}
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
for (int i = 0; i < extra; ++i) {
output.write(0);
}
input.close();
} catch (IOException e) {
fail();
}
}
private byte[] getAsByteArray(int resId) {
return getAsByteArray(resId, 0, 0);
}
private ByteBuffer getAsByteBufferWrap(int resId) {
byte[] buffer = getAsByteArray(resId);
return ByteBuffer.wrap(buffer);
}
private ByteBuffer getAsDirectByteBuffer(int resId) {
byte[] buffer = getAsByteArray(resId);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(buffer.length);
byteBuffer.put(buffer);
byteBuffer.position(0);
return byteBuffer;
}
private ByteBuffer getAsReadOnlyByteBuffer(int resId) {
return getAsByteBufferWrap(resId).asReadOnlyBuffer();
}
private File getAsFile(int resId) {
File file = null;
try {
Context context = InstrumentationRegistry.getTargetContext();
File dir = new File(context.getFilesDir(), "images");
dir.mkdirs();
file = new File(dir, "test_file" + resId);
if (!file.createNewFile()) {
if (file.exists()) {
return file;
}
fail("Failed to create new File!");
}
FileOutputStream output = new FileOutputStream(file);
writeToStream(output, resId, 0, 0);
output.close();
} catch (IOException e) {
fail("Failed with exception " + e);
return null;
}
return file;
}
private Uri getAsFileUri(int resId) {
return Uri.fromFile(getAsFile(resId));
}
private Uri getAsContentUri(int resId) {
Context context = InstrumentationRegistry.getTargetContext();
return FileProvider.getUriForFile(context,
"android.graphics.cts.fileprovider", getAsFile(resId));
}
private Uri getAsResourceUri(int resId) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
.authority(mRes.getResourcePackageName(resId))
.appendPath(mRes.getResourceTypeName(resId))
.appendPath(mRes.getResourceEntryName(resId))
.build();
}
private interface SourceCreator extends IntFunction<ImageDecoder.Source> {};
private SourceCreator[] mCreators = new SourceCreator[] {
resId -> ImageDecoder.createSource(getAsByteBufferWrap(resId)),
resId -> ImageDecoder.createSource(getAsDirectByteBuffer(resId)),
resId -> ImageDecoder.createSource(getAsReadOnlyByteBuffer(resId)),
resId -> ImageDecoder.createSource(getAsFile(resId)),
};
private interface UriCreator extends IntFunction<Uri> {};
private UriCreator[] mUriCreators = new UriCreator[] {
resId -> getAsResourceUri(resId),
resId -> getAsFileUri(resId),
resId -> getAsContentUri(resId),
};
@Test
public void testUris() {
for (Record record : RECORDS) {
int resId = record.resId;
String name = mRes.getResourceEntryName(resId);
for (UriCreator f : mUriCreators) {
ImageDecoder.Source src = null;
Uri uri = f.apply(resId);
String fullName = name + ": " + uri.toString();
src = ImageDecoder.createSource(mContentResolver, uri);
assertNotNull("failed to create Source for " + fullName, src);
try {
Drawable d = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setOnPartialImageListener((e) -> {
fail("error for image " + fullName + ":\n" + e);
return false;
});
});
assertNotNull("failed to create drawable for " + fullName, d);
} catch (IOException e) {
fail("exception for image " + fullName + ":\n" + e);
}
}
}
}
@Before
public void setup() {
mRes = InstrumentationRegistry.getTargetContext().getResources();
mContentResolver = InstrumentationRegistry.getTargetContext().getContentResolver();
}
@Test
public void testInfo() {
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
ImageDecoder.Source src = f.apply(record.resId);
assertNotNull(src);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertEquals(record.width, info.getSize().getWidth());
assertEquals(record.height, info.getSize().getHeight());
assertEquals(record.mimeType, info.getMimeType());
assertSame(record.colorSpace, info.getColorSpace());
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
}
}
}
}
@Test
public void testDecodeDrawable() {
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
ImageDecoder.Source src = f.apply(record.resId);
assertNotNull(src);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src);
assertNotNull(drawable);
assertEquals(record.width, drawable.getIntrinsicWidth());
assertEquals(record.height, drawable.getIntrinsicHeight());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
@Test
public void testDecodeBitmap() {
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
ImageDecoder.Source src = f.apply(record.resId);
assertNotNull(src);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src);
assertNotNull(bm);
assertEquals(record.width, bm.getWidth());
assertEquals(record.height, bm.getHeight());
assertFalse(bm.isMutable());
// FIXME: This may change for small resources, etc.
assertEquals(Bitmap.Config.HARDWARE, bm.getConfig());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
@Test(expected=IllegalArgumentException.class)
public void testSetBogusAllocator() {
ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId);
try {
ImageDecoder.decodeBitmap(src, (decoder, info, s) -> decoder.setAllocator(15));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
private static final int[] ALLOCATORS = new int[] {
ImageDecoder.ALLOCATOR_SOFTWARE,
ImageDecoder.ALLOCATOR_SHARED_MEMORY,
ImageDecoder.ALLOCATOR_HARDWARE,
ImageDecoder.ALLOCATOR_DEFAULT,
};
@Test
public void testGetAllocator() {
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertEquals(ImageDecoder.ALLOCATOR_DEFAULT, decoder.getAllocator());
for (int allocator : ALLOCATORS) {
decoder.setAllocator(allocator);
assertEquals(allocator, decoder.getAllocator());
}
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testSetAllocatorDecodeBitmap() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
public int allocator;
public boolean doCrop;
public boolean doScale;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
decoder.setAllocator(allocator);
if (doScale) {
decoder.setTargetSampleSize(2);
}
if (doCrop) {
decoder.setCrop(new Rect(1, 1, info.getSize().getWidth() / 2 - 1,
info.getSize().getHeight() / 2 - 1));
}
}
};
Listener l = new Listener();
boolean trueFalse[] = new boolean[] { true, false };
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (int allocator : ALLOCATORS) {
for (boolean doCrop : trueFalse) {
for (boolean doScale : trueFalse) {
l.doCrop = doCrop;
l.doScale = doScale;
l.allocator = allocator;
ImageDecoder.Source src = f.apply(record.resId);
assertNotNull(src);
Bitmap bm = null;
try {
bm = ImageDecoder.decodeBitmap(src, l);
} catch (IOException e) {
fail("Failed " + getAsResourceUri(record.resId) +
" with exception " + e);
}
assertNotNull(bm);
switch (allocator) {
case ImageDecoder.ALLOCATOR_SOFTWARE:
// TODO: Once Bitmap provides access to its
// SharedMemory, confirm that ALLOCATOR_SHARED_MEMORY
// worked.
case ImageDecoder.ALLOCATOR_SHARED_MEMORY:
assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
if (!doScale && !doCrop) {
Bitmap reference = BitmapFactory.decodeResource(mRes,
record.resId, null);
assertNotNull(reference);
BitmapUtils.compareBitmaps(bm, reference);
}
break;
default:
String name = getAsResourceUri(record.resId).toString();
assertEquals("image " + name + "; allocator: " + allocator,
Bitmap.Config.HARDWARE, bm.getConfig());
break;
}
}
}
}
}
}
}
@Test
public void testGetUnpremul() {
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
assertFalse(decoder.isUnpremultipliedRequired());
decoder.setUnpremultipliedRequired(true);
assertTrue(decoder.isUnpremultipliedRequired());
decoder.setUnpremultipliedRequired(false);
assertFalse(decoder.isUnpremultipliedRequired());
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testUnpremul() {
int[] resIds = new int[] { R.drawable.png_test, R.drawable.alpha };
boolean[] hasAlpha = new boolean[] { false, true };
for (int i = 0; i < resIds.length; ++i) {
for (SourceCreator f : mCreators) {
// Normal decode
ImageDecoder.Source src = f.apply(resIds[i]);
assertNotNull(src);
try {
Bitmap normal = ImageDecoder.decodeBitmap(src);
assertNotNull(normal);
assertEquals(normal.hasAlpha(), hasAlpha[i]);
assertEquals(normal.isPremultiplied(), hasAlpha[i]);
// Require unpremul
src = f.apply(resIds[i]);
assertNotNull(src);
Bitmap unpremul = ImageDecoder.decodeBitmap(src,
(decoder, info, s) -> decoder.setUnpremultipliedRequired(true));
assertNotNull(unpremul);
assertEquals(unpremul.hasAlpha(), hasAlpha[i]);
assertFalse(unpremul.isPremultiplied());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
@Test
public void testGetPostProcessor() {
PostProcessor[] processors = new PostProcessor[] {
(canvas) -> PixelFormat.UNKNOWN,
(canvas) -> PixelFormat.UNKNOWN,
null,
};
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertNull(decoder.getPostProcessor());
for (PostProcessor pp : processors) {
decoder.setPostProcessor(pp);
assertSame(pp, decoder.getPostProcessor());
}
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testPostProcessor() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
public boolean requireSoftware;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
if (requireSoftware) {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
decoder.setPostProcessor((canvas) -> {
canvas.drawColor(Color.BLACK);
return PixelFormat.OPAQUE;
});
}
};
Listener l = new Listener();
boolean trueFalse[] = new boolean[] { true, false };
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (boolean requireSoftware : trueFalse) {
l.requireSoftware = requireSoftware;
ImageDecoder.Source src = f.apply(record.resId);
assertNotNull(src);
Bitmap bitmap = null;
try {
bitmap = ImageDecoder.decodeBitmap(src, l);
} catch (IOException e) {
fail("Failed with exception " + e);
}
assertNotNull(bitmap);
assertFalse(bitmap.isMutable());
if (requireSoftware) {
assertNotEquals(Bitmap.Config.HARDWARE, bitmap.getConfig());
for (int x = 0; x < bitmap.getWidth(); ++x) {
for (int y = 0; y < bitmap.getHeight(); ++y) {
int color = bitmap.getPixel(x, y);
assertEquals("pixel at (" + x + ", " + y + ") does not match!",
color, Color.BLACK);
}
}
} else {
assertEquals(bitmap.getConfig(), Bitmap.Config.HARDWARE);
}
}
}
}
}
@Test
public void testNinepatchWithDensityNone() {
TypedValue value = new TypedValue();
InputStream is = mRes.openRawResource(R.drawable.ninepatch_nodpi, value);
// This does not call ImageDecoder directly because this entry point is not public.
Drawable dr = Drawable.createFromResourceStream(mRes, value, is, null, null);
assertNotNull(dr);
assertEquals(5, dr.getIntrinsicWidth());
assertEquals(5, dr.getIntrinsicHeight());
}
@Test
public void testPostProcessorOverridesNinepatch() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
public boolean requireSoftware;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
if (requireSoftware) {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN);
}
};
Listener l = new Listener();
int resIds[] = new int[] { R.drawable.ninepatch_0,
R.drawable.ninepatch_1 };
boolean trueFalse[] = new boolean[] { true, false };
for (int resId : resIds) {
for (SourceCreator f : mCreators) {
for (boolean requireSoftware : trueFalse) {
l.requireSoftware = requireSoftware;
ImageDecoder.Source src = f.apply(resId);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src, l);
assertFalse(drawable instanceof NinePatchDrawable);
src = f.apply(resId);
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertNull(bm.getNinePatchChunk());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
}
@Test
public void testPostProcessorAndMadeOpaque() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
public boolean requireSoftware;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
if (requireSoftware) {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
decoder.setPostProcessor((c) -> PixelFormat.OPAQUE);
}
};
Listener l = new Listener();
boolean trueFalse[] = new boolean[] { true, false };
int resIds[] = new int[] { R.drawable.alpha, R.drawable.google_logo_2 };
for (int resId : resIds) {
for (SourceCreator f : mCreators) {
for (boolean requireSoftware : trueFalse) {
l.requireSoftware = requireSoftware;
ImageDecoder.Source src = f.apply(resId);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertFalse(bm.hasAlpha());
assertFalse(bm.isPremultiplied());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
}
@Test
public void testPostProcessorAndAddedTransparency() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
public boolean requireSoftware;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
if (requireSoftware) {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
decoder.setPostProcessor((c) -> PixelFormat.TRANSLUCENT);
}
};
Listener l = new Listener();
boolean trueFalse[] = new boolean[] { true, false };
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (boolean requireSoftware : trueFalse) {
l.requireSoftware = requireSoftware;
ImageDecoder.Source src = f.apply(record.resId);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertTrue(bm.hasAlpha());
assertTrue(bm.isPremultiplied());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
}
@Test(expected=IllegalArgumentException.class)
public void testPostProcessorTRANSPARENT() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setPostProcessor((c) -> PixelFormat.TRANSPARENT);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalArgumentException.class)
public void testPostProcessorInvalidReturn() {
ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setPostProcessor((c) -> 42);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testPostProcessorAndUnpremul() {
ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setUnpremultipliedRequired(true);
decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test
public void testPostProcessorAndScale() {
class PostProcessorWithSize implements PostProcessor {
public int width;
public int height;
@Override
public int onPostProcess(Canvas canvas) {
assertEquals(this.width, width);
assertEquals(this.height, height);
return PixelFormat.UNKNOWN;
};
};
final PostProcessorWithSize pp = new PostProcessorWithSize();
for (Record record : RECORDS) {
pp.width = record.width / 2;
pp.height = record.height / 2;
for (SourceCreator f : mCreators) {
ImageDecoder.Source src = f.apply(record.resId);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setTargetSize(pp.width, pp.height);
decoder.setPostProcessor(pp);
});
assertEquals(pp.width, drawable.getIntrinsicWidth());
assertEquals(pp.height, drawable.getIntrinsicHeight());
} catch (IOException e) {
fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
}
}
}
}
private void checkSampleSize(String name, int originalDimension, int sampleSize, int result) {
if (originalDimension % sampleSize == 0) {
assertEquals("Mismatch for " + name + ": " + originalDimension + " / " + sampleSize
+ " != " + result, originalDimension / sampleSize, result);
} else if (originalDimension <= sampleSize) {
assertEquals(1, result);
} else {
// Rounding may result in differences.
int size = result * sampleSize;
assertTrue("Rounding mismatch for " + name + ": " + originalDimension + " / "
+ sampleSize + " = " + result,
Math.abs(size - originalDimension) < sampleSize);
}
}
@Test
public void testSampleSize() {
for (Record record : RECORDS) {
final String name = getAsResourceUri(record.resId).toString();
for (int sampleSize : new int[] { 2, 3, 4, 8, 32 }) {
ImageDecoder.Source src = mCreators[0].apply(record.resId);
try {
Drawable dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setTargetSampleSize(sampleSize);
});
checkSampleSize(name, record.width, sampleSize, dr.getIntrinsicWidth());
checkSampleSize(name, record.height, sampleSize, dr.getIntrinsicHeight());
} catch (IOException e) {
fail("Failed " + name + " with exception " + e);
}
}
}
}
private interface SampleSizeSupplier extends ToIntFunction<Size> {};
@Test
public void testLargeSampleSize() {
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (SampleSizeSupplier supplySampleSize : new SampleSizeSupplier[] {
(size) -> size.getWidth(),
(size) -> size.getWidth() + 5,
(size) -> size.getWidth() * 5,
}) {
ImageDecoder.Source src = f.apply(record.resId);
try {
Drawable dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
int sampleSize = supplySampleSize.applyAsInt(info.getSize());
decoder.setTargetSampleSize(sampleSize);
});
assertEquals(1, dr.getIntrinsicWidth());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
}
@Test
public void testResizeTransparency() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.animated);
Drawable dr = null;
try {
dr = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
Size size = info.getSize();
decoder.setTargetSize(size.getWidth() - 5, size.getHeight() - 5);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
final int width = dr.getIntrinsicWidth();
final int height = dr.getIntrinsicHeight();
// Draw to a fully transparent Bitmap. Pixels that are transparent in the image will be
// transparent.
Bitmap normal = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
{
Canvas canvas = new Canvas(normal);
dr.draw(canvas);
}
// Draw to a BLUE Bitmap. Any pixels that are transparent in the image remain BLUE.
Bitmap blended = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
{
Canvas canvas = new Canvas(blended);
canvas.drawColor(Color.BLUE);
dr.draw(canvas);
}
boolean hasTransparency = false;
for (int i = 0; i < width; ++i) {
for (int j = 0; j < height; ++j) {
int normalColor = normal.getPixel(i, j);
int blendedColor = blended.getPixel(i, j);
if (normalColor == Color.TRANSPARENT) {
hasTransparency = true;
assertEquals(Color.BLUE, blendedColor);
} else if (Color.alpha(normalColor) == 255) {
assertEquals(normalColor, blendedColor);
}
}
}
// Verify that the image has transparency. Otherwise the test is not useful.
assertTrue(hasTransparency);
}
@Test
public void testGetOnPartialImageListener() {
OnPartialImageListener[] listeners = new OnPartialImageListener[] {
(e) -> true,
(e) -> false,
null,
};
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertNull(decoder.getOnPartialImageListener());
for (OnPartialImageListener l : listeners) {
decoder.setOnPartialImageListener(l);
assertSame(l, decoder.getOnPartialImageListener());
}
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testEarlyIncomplete() {
byte[] bytes = getAsByteArray(R.raw.basi6a16);
// This is too early to create a partial image, so we throw the Exception
// without calling the listener.
int truncatedLength = 49;
ImageDecoder.Source src = ImageDecoder.createSource(
ByteBuffer.wrap(bytes, 0, truncatedLength));
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setOnPartialImageListener((e) -> {
fail("No need to call listener; no partial image to display!"
+ " Exception: " + e);
return false;
});
});
} catch (DecodeException e) {
assertEquals(DecodeException.SOURCE_INCOMPLETE, e.getError());
assertSame(src, e.getSource());
} catch (IOException ioe) {
fail("Threw some other exception: " + ioe);
}
}
private class ExceptionStream extends InputStream {
private final InputStream mInputStream;
private final int mExceptionPosition;
int mPosition;
ExceptionStream(int resId, int exceptionPosition) {
mInputStream = mRes.openRawResource(resId);
mExceptionPosition = exceptionPosition;
mPosition = 0;
}
@Override
public int read() throws IOException {
if (mPosition >= mExceptionPosition) {
throw new IOException();
}
int value = mInputStream.read();
mPosition++;
return value;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (mPosition + len <= mExceptionPosition) {
final int bytesRead = mInputStream.read(b, off, len);
mPosition += bytesRead;
return bytesRead;
}
len = mExceptionPosition - mPosition;
mPosition += mInputStream.read(b, off, len);
throw new IOException();
}
}
@Test
public void testExceptionInStream() throws Throwable {
InputStream is = new ExceptionStream(R.drawable.animated, 27570);
ImageDecoder.Source src = ImageDecoder.createSource(mRes, is, Bitmap.DENSITY_NONE);
Drawable dr = null;
try {
dr = ImageDecoder.decodeDrawable(src);
fail("Expected to throw an exception!");
} catch (IOException ioe) {
assertTrue(ioe instanceof DecodeException);
DecodeException decodeException = (DecodeException) ioe;
assertEquals(DecodeException.SOURCE_EXCEPTION, decodeException.getError());
Throwable throwable = decodeException.getCause();
assertNotNull(throwable);
assertTrue(throwable instanceof IOException);
}
assertNull(dr);
}
@Test
public void testOnPartialImage() {
class PartialImageCallback implements OnPartialImageListener {
public boolean wasCalled;
public boolean returnDrawable;
public ImageDecoder.Source source;
@Override
public boolean onPartialImage(DecodeException e) {
wasCalled = true;
assertEquals(DecodeException.SOURCE_INCOMPLETE, e.getError());
assertSame(source, e.getSource());
return returnDrawable;
}
};
final PartialImageCallback callback = new PartialImageCallback();
boolean abortDecode[] = new boolean[] { true, false };
for (Record record : RECORDS) {
byte[] bytes = getAsByteArray(record.resId);
int truncatedLength = bytes.length / 2;
if (record.mimeType.equals("image/x-ico")
|| record.mimeType.equals("image/x-adobe-dng")) {
// FIXME (scroggo): Some codecs currently do not support incomplete images.
continue;
}
for (boolean abort : abortDecode) {
ImageDecoder.Source src = ImageDecoder.createSource(
ByteBuffer.wrap(bytes, 0, truncatedLength));
callback.wasCalled = false;
callback.returnDrawable = !abort;
callback.source = src;
try {
Drawable drawable = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setOnPartialImageListener(callback);
});
assertFalse(abort);
assertNotNull(drawable);
assertEquals(record.width, drawable.getIntrinsicWidth());
assertEquals(record.height, drawable.getIntrinsicHeight());
} catch (IOException e) {
assertTrue(abort);
}
assertTrue(callback.wasCalled);
}
// null listener behaves as if onPartialImage returned false.
ImageDecoder.Source src = ImageDecoder.createSource(
ByteBuffer.wrap(bytes, 0, truncatedLength));
try {
ImageDecoder.decodeDrawable(src);
fail("Should have thrown an exception!");
} catch (DecodeException incomplete) {
// This is the correct behavior.
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
@Test
public void testCorruptException() {
class PartialImageCallback implements OnPartialImageListener {
public boolean wasCalled = false;
public ImageDecoder.Source source;
@Override
public boolean onPartialImage(DecodeException e) {
wasCalled = true;
assertEquals(DecodeException.SOURCE_MALFORMED_DATA, e.getError());
assertSame(source, e.getSource());
return true;
}
};
final PartialImageCallback callback = new PartialImageCallback();
byte[] bytes = getAsByteArray(R.drawable.png_test);
// The four bytes starting with byte 40,000 represent the CRC. Changing
// them will cause the decode to fail.
for (int i = 0; i < 4; ++i) {
bytes[40000 + i] = 'X';
}
ImageDecoder.Source src = ImageDecoder.createSource(ByteBuffer.wrap(bytes));
callback.wasCalled = false;
callback.source = src;
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setOnPartialImageListener(callback);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
assertTrue(callback.wasCalled);
}
private static class DummyException extends RuntimeException {};
@Test
public void testPartialImageThrowException() {
byte[] bytes = getAsByteArray(R.drawable.png_test);
ImageDecoder.Source src = ImageDecoder.createSource(
ByteBuffer.wrap(bytes, 0, bytes.length / 2));
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setOnPartialImageListener((e) -> {
throw new DummyException();
});
});
fail("Should have thrown an exception");
} catch (DummyException dummy) {
// This is correct.
} catch (Throwable t) {
fail("Should have thrown DummyException - threw " + t + " instead");
}
}
@Test
public void testGetMutable() {
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertFalse(decoder.isMutableRequired());
decoder.setMutableRequired(true);
assertTrue(decoder.isMutableRequired());
decoder.setMutableRequired(false);
assertFalse(decoder.isMutableRequired());
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testMutable() {
int allocators[] = new int[] { ImageDecoder.ALLOCATOR_DEFAULT,
ImageDecoder.ALLOCATOR_SOFTWARE,
ImageDecoder.ALLOCATOR_SHARED_MEMORY };
class HeaderListener implements ImageDecoder.OnHeaderDecodedListener {
int allocator;
boolean postProcess;
@Override
public void onHeaderDecoded(ImageDecoder decoder,
ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
decoder.setMutableRequired(true);
decoder.setAllocator(allocator);
if (postProcess) {
decoder.setPostProcessor((c) -> PixelFormat.UNKNOWN);
}
}
};
HeaderListener l = new HeaderListener();
boolean trueFalse[] = new boolean[] { true, false };
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (boolean postProcess : trueFalse) {
for (int allocator : allocators) {
l.allocator = allocator;
l.postProcess = postProcess;
ImageDecoder.Source src = f.apply(record.resId);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertTrue(bm.isMutable());
assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
}
}
@Test(expected=IllegalStateException.class)
public void testMutableHardware() {
ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId);
try {
ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
decoder.setMutableRequired(true);
decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testMutableDrawable() {
ImageDecoder.Source src = mCreators[0].apply(RECORDS[0].resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setMutableRequired(true);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
private interface EmptyByteBufferCreator {
public ByteBuffer apply();
};
@Test
public void testEmptyByteBuffer() {
class Direct implements EmptyByteBufferCreator {
@Override
public ByteBuffer apply() {
return ByteBuffer.allocateDirect(0);
}
};
class Wrap implements EmptyByteBufferCreator {
@Override
public ByteBuffer apply() {
byte[] bytes = new byte[0];
return ByteBuffer.wrap(bytes);
}
};
class ReadOnly implements EmptyByteBufferCreator {
@Override
public ByteBuffer apply() {
byte[] bytes = new byte[0];
return ByteBuffer.wrap(bytes).asReadOnlyBuffer();
}
};
EmptyByteBufferCreator creators[] = new EmptyByteBufferCreator[] {
new Direct(), new Wrap(), new ReadOnly() };
for (EmptyByteBufferCreator creator : creators) {
try {
ImageDecoder.decodeDrawable(
ImageDecoder.createSource(creator.apply()));
fail("This should have thrown an exception");
} catch (IOException e) {
// This is correct.
}
}
}
@Test(expected=IllegalArgumentException.class)
public void testZeroSampleSize() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> decoder.setTargetSampleSize(0));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalArgumentException.class)
public void testNegativeSampleSize() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> decoder.setTargetSampleSize(-2));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test
public void testTargetSize() {
class ResizeListener implements ImageDecoder.OnHeaderDecodedListener {
public int width;
public int height;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
decoder.setTargetSize(width, height);
}
};
ResizeListener l = new ResizeListener();
float[] scales = new float[] { .0625f, .125f, .25f, .5f, .75f, 1.1f, 2.0f };
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (int j = 0; j < scales.length; ++j) {
l.width = (int) (scales[j] * record.width);
l.height = (int) (scales[j] * record.height);
ImageDecoder.Source src = f.apply(record.resId);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src, l);
assertEquals(l.width, drawable.getIntrinsicWidth());
assertEquals(l.height, drawable.getIntrinsicHeight());
src = f.apply(record.resId);
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertEquals(l.width, bm.getWidth());
assertEquals(l.height, bm.getHeight());
} catch (IOException e) {
fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
}
}
try {
// Arbitrary square.
l.width = 50;
l.height = 50;
ImageDecoder.Source src = f.apply(record.resId);
Drawable drawable = ImageDecoder.decodeDrawable(src, l);
assertEquals(50, drawable.getIntrinsicWidth());
assertEquals(50, drawable.getIntrinsicHeight());
// Swap width and height, for different scales.
l.height = record.width;
l.width = record.height;
src = f.apply(record.resId);
drawable = ImageDecoder.decodeDrawable(src, l);
assertEquals(record.height, drawable.getIntrinsicWidth());
assertEquals(record.width, drawable.getIntrinsicHeight());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
@Test
public void testResizeWebp() {
// libwebp supports unpremultiplied for downscaled output
class ResizeListener implements ImageDecoder.OnHeaderDecodedListener {
public int width;
public int height;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
decoder.setTargetSize(width, height);
decoder.setUnpremultipliedRequired(true);
}
};
ResizeListener l = new ResizeListener();
float[] scales = new float[] { .0625f, .125f, .25f, .5f, .75f };
for (SourceCreator f : mCreators) {
for (int j = 0; j < scales.length; ++j) {
l.width = (int) (scales[j] * 240);
l.height = (int) (scales[j] * 87);
ImageDecoder.Source src = f.apply(R.drawable.google_logo_2);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertEquals(l.width, bm.getWidth());
assertEquals(l.height, bm.getHeight());
assertTrue(bm.hasAlpha());
assertFalse(bm.isPremultiplied());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
@Test(expected=IllegalStateException.class)
public void testResizeWebpLarger() {
// libwebp does not upscale, so there is no way to get unpremul.
ImageDecoder.Source src = mCreators[0].apply(R.drawable.google_logo_2);
try {
ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
Size size = info.getSize();
decoder.setTargetSize(size.getWidth() * 2, size.getHeight() * 2);
decoder.setUnpremultipliedRequired(true);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testResizeUnpremul() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.alpha);
try {
ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
// Choose a width and height that cannot be achieved with sampling.
Size size = info.getSize();
int width = size.getWidth() / 2 + 3;
int height = size.getHeight() / 2 + 3;
decoder.setTargetSize(width, height);
decoder.setUnpremultipliedRequired(true);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test
public void testGetCrop() {
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertNull(decoder.getCrop());
Rect r = new Rect(0, 0, info.getSize().getWidth() / 2, 5);
decoder.setCrop(r);
assertEquals(r, decoder.getCrop());
r = new Rect(0, 0, 5, 10);
decoder.setCrop(r);
assertEquals(r, decoder.getCrop());
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testCrop() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
public boolean doScale;
public boolean requireSoftware;
public Rect cropRect;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
int width = info.getSize().getWidth();
int height = info.getSize().getHeight();
if (doScale) {
width /= 2;
height /= 2;
decoder.setTargetSize(width, height);
}
// Crop to the middle:
int quarterWidth = width / 4;
int quarterHeight = height / 4;
cropRect = new Rect(quarterWidth, quarterHeight,
quarterWidth * 3, quarterHeight * 3);
decoder.setCrop(cropRect);
if (requireSoftware) {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
}
};
Listener l = new Listener();
boolean trueFalse[] = new boolean[] { true, false };
for (Record record : RECORDS) {
for (SourceCreator f : mCreators) {
for (boolean doScale : trueFalse) {
l.doScale = doScale;
for (boolean requireSoftware : trueFalse) {
l.requireSoftware = requireSoftware;
ImageDecoder.Source src = f.apply(record.resId);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src, l);
assertEquals(l.cropRect.width(), drawable.getIntrinsicWidth());
assertEquals(l.cropRect.height(), drawable.getIntrinsicHeight());
} catch (IOException e) {
fail("Failed " + getAsResourceUri(record.resId) +
" with exception " + e);
}
}
}
}
}
}
@Test(expected=IllegalArgumentException.class)
public void testResizeZeroX() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
decoder.setTargetSize(0, info.getSize().getHeight()));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalArgumentException.class)
public void testResizeZeroY() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
decoder.setTargetSize(info.getSize().getWidth(), 0));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalArgumentException.class)
public void testResizeNegativeX() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
decoder.setTargetSize(-10, info.getSize().getHeight()));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalArgumentException.class)
public void testResizeNegativeY() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
decoder.setTargetSize(info.getSize().getWidth(), -10));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testStoreImageDecoder() {
class CachingCallback implements ImageDecoder.OnHeaderDecodedListener {
ImageDecoder cachedDecoder;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
cachedDecoder = decoder;
}
};
CachingCallback l = new CachingCallback();
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, l);
} catch (IOException e) {
fail("Failed with exception " + e);
}
l.cachedDecoder.setTargetSampleSize(2);
}
@Test(expected=IllegalStateException.class)
public void testDecodeUnpremulDrawable() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) ->
decoder.setUnpremultipliedRequired(true));
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testCropNegativeLeft() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setCrop(new Rect(-1, 0, info.getSize().getWidth(),
info.getSize().getHeight()));
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testCropNegativeTop() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setCrop(new Rect(0, -1, info.getSize().getWidth(),
info.getSize().getHeight()));
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testCropTooWide() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setCrop(new Rect(1, 0, info.getSize().getWidth() + 1,
info.getSize().getHeight()));
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testCropTooTall() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setCrop(new Rect(0, 1, info.getSize().getWidth(),
info.getSize().getHeight() + 1));
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testCropResize() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
Size size = info.getSize();
decoder.setTargetSize(size.getWidth() / 2, size.getHeight() / 2);
decoder.setCrop(new Rect(0, 0, size.getWidth(),
size.getHeight()));
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test
public void testAlphaMaskNonGray() {
// It is safe to call setDecodeAsAlphaMaskEnabled on a non-gray image.
SourceCreator f = mCreators[0];
ImageDecoder.Source src = f.apply(R.drawable.png_test);
assertNotNull(src);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
decoder.setDecodeAsAlphaMaskEnabled(true);
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
assertNotNull(bm);
assertNotEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test(expected=IllegalStateException.class)
public void testAlphaMaskPlusHardware() {
SourceCreator f = mCreators[0];
ImageDecoder.Source src = f.apply(R.drawable.png_test);
assertNotNull(src);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setDecodeAsAlphaMaskEnabled(true);
decoder.setAllocator(ImageDecoder.ALLOCATOR_HARDWARE);
});
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
@Test
public void testGetAlphaMask() {
final int resId = R.drawable.grayscale_png;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertFalse(decoder.isDecodeAsAlphaMaskEnabled());
decoder.setDecodeAsAlphaMaskEnabled(true);
assertTrue(decoder.isDecodeAsAlphaMaskEnabled());
decoder.setDecodeAsAlphaMaskEnabled(false);
assertFalse(decoder.isDecodeAsAlphaMaskEnabled());
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testAlphaMask() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
boolean doCrop;
boolean doScale;
boolean doPostProcess;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
decoder.setDecodeAsAlphaMaskEnabled(true);
Size size = info.getSize();
if (doScale) {
decoder.setTargetSize(size.getWidth() / 2,
size.getHeight() / 2);
}
if (doCrop) {
decoder.setCrop(new Rect(0, 0, size.getWidth() / 4,
size.getHeight() / 4));
}
if (doPostProcess) {
decoder.setPostProcessor((c) -> {
c.drawColor(Color.BLACK);
return PixelFormat.UNKNOWN;
});
}
}
};
Listener l = new Listener();
// Both of these are encoded as single channel gray images.
int resIds[] = new int[] { R.drawable.grayscale_png, R.drawable.grayscale_jpg };
boolean trueFalse[] = new boolean[] { true, false };
SourceCreator f = mCreators[0];
for (int resId : resIds) {
// By default, this will decode to HARDWARE
ImageDecoder.Source src = f.apply(resId);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src);
assertEquals(Bitmap.Config.HARDWARE, bm.getConfig());
} catch (IOException e) {
fail("Failed with exception " + e);
}
// Now set alpha mask, which is incompatible with HARDWARE
for (boolean doCrop : trueFalse) {
for (boolean doScale : trueFalse) {
for (boolean doPostProcess : trueFalse) {
l.doCrop = doCrop;
l.doScale = doScale;
l.doPostProcess = doPostProcess;
src = f.apply(resId);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, l);
assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
} catch (IOException e) {
fail("Failed with exception " + e);
}
}
}
}
}
}
@Test
public void testGetConserveMemory() {
final int resId = RECORDS[0].resId;
ImageDecoder.Source src = mCreators[0].apply(resId);
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy());
decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
assertEquals(ImageDecoder.MEMORY_POLICY_LOW_RAM, decoder.getMemorySizePolicy());
decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_DEFAULT);
assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy());
});
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
}
}
@Test
public void testConserveMemoryPlusHardware() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
int allocator;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
decoder.setAllocator(allocator);
}
};
Listener l = new Listener();
SourceCreator f = mCreators[0];
for (int resId : new int[] { R.drawable.png_test, R.raw.basi6a16 }) {
Bitmap normal = null;
try {
normal = ImageDecoder.decodeBitmap(f.apply(resId));
} catch (IOException e) {
fail("Failed with exception " + e);
}
assertNotNull(normal);
int normalByteCount = normal.getAllocationByteCount();
for (int allocator : ALLOCATORS) {
l.allocator = allocator;
Bitmap test = null;
try {
test = ImageDecoder.decodeBitmap(f.apply(resId), l);
} catch (IOException e) {
fail("Failed with exception " + e);
}
assertNotNull(test);
int byteCount = test.getAllocationByteCount();
if (allocator == ImageDecoder.ALLOCATOR_HARDWARE
|| allocator == ImageDecoder.ALLOCATOR_DEFAULT) {
if (resId == R.drawable.png_test) {
// We do not support 565 in HARDWARE, so no RAM savings
// are possible.
assertEquals(normalByteCount, byteCount);
} else { // R.raw.basi6a16
// This image defaults to F16. MEMORY_POLICY_LOW_RAM
// forces "test" to decode to 8888. But if the device
// does not support F16 in HARDWARE, "normal" is also
// 8888. Its Config is HARDWARE either way, but we can
// detect its underlying pixel format by checking the
// ColorSpace, which is always LINEAR_EXTENDED_SRGB for
// F16.
if (normal.getColorSpace().equals(
ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) {
// F16. "test" should be smaller.
assertTrue(byteCount < normalByteCount);
} else {
// 8888. No RAM savings possible.
assertEquals(normalByteCount, byteCount);
}
}
} else {
// Not decoding to HARDWARE, but |normal| was. Again, if basi6a16
// was decoded to 8888, which we can detect by looking at the color
// space, no savings are possible.
if (resId == R.raw.basi6a16 && !normal.getColorSpace().equals(
ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB))) {
assertEquals(normalByteCount, byteCount);
} else {
assertTrue(byteCount < normalByteCount);
}
}
}
}
}
@Test
public void testConserveMemory() {
class Listener implements ImageDecoder.OnHeaderDecodedListener {
boolean doPostProcess;
boolean preferRamOverQuality;
@Override
public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
ImageDecoder.Source src) {
if (preferRamOverQuality) {
decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
}
if (doPostProcess) {
decoder.setPostProcessor((c) -> {
c.drawColor(Color.BLACK);
return PixelFormat.TRANSLUCENT;
});
}
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
};
Listener l = new Listener();
// All of these images are opaque, so they can save RAM with
// setConserveMemory.
int resIds[] = new int[] { R.drawable.png_test, R.drawable.baseline_jpeg,
// If this were stored in drawable/, it would
// be converted from 16-bit to 8. FIXME: Is
// behavior still desirable now that we have
// F16?
R.raw.basi6a16 };
// An opaque image can be converted to 565, but postProcess will promote
// to 8888 in case alpha is added. The third image defaults to F16, so
// even with postProcess it will only be promoted to 8888.
boolean postProcessCancels[] = new boolean[] { true, true, false };
boolean trueFalse[] = new boolean[] { true, false };
SourceCreator f = mCreators[0];
for (int i = 0; i < resIds.length; ++i) {
int resId = resIds[i];
l.doPostProcess = false;
l.preferRamOverQuality = false;
Bitmap normal = null;
try {
normal = ImageDecoder.decodeBitmap(f.apply(resId), l);
} catch (IOException e) {
fail("Failed with exception " + e);
}
int normalByteCount = normal.getAllocationByteCount();
for (boolean doPostProcess : trueFalse) {
l.doPostProcess = doPostProcess;
l.preferRamOverQuality = true;
Bitmap saveRamOverQuality = null;
try {
saveRamOverQuality = ImageDecoder.decodeBitmap(f.apply(resId), l);
} catch (IOException e) {
fail("Failed with exception " + e);
}
int saveByteCount = saveRamOverQuality.getAllocationByteCount();
if (doPostProcess && postProcessCancels[i]) {
// Promoted to normal.
assertEquals(normalByteCount, saveByteCount);
} else {
assertTrue(saveByteCount < normalByteCount);
}
}
}
}
@Test
public void testRespectOrientation() {
// These 8 images test the 8 EXIF orientations. If the orientation is
// respected, they all have the same dimensions: 100 x 80.
// They are also identical (after adjusting), so compare them.
Bitmap reference = null;
for (int resId : new int[] { R.drawable.orientation_1,
R.drawable.orientation_2,
R.drawable.orientation_3,
R.drawable.orientation_4,
R.drawable.orientation_5,
R.drawable.orientation_6,
R.drawable.orientation_7,
R.drawable.orientation_8,
R.drawable.webp_orientation1,
R.drawable.webp_orientation2,
R.drawable.webp_orientation3,
R.drawable.webp_orientation4,
R.drawable.webp_orientation5,
R.drawable.webp_orientation6,
R.drawable.webp_orientation7,
R.drawable.webp_orientation8,
}) {
if (resId == R.drawable.webp_orientation1) {
// The webp files may not look exactly the same as the jpegs.
// Recreate the reference.
reference = null;
}
Uri uri = getAsResourceUri(resId);
ImageDecoder.Source src = ImageDecoder.createSource(mContentResolver, uri);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
// Use software allocator so we can compare.
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
assertNotNull(bm);
assertEquals(100, bm.getWidth());
assertEquals(80, bm.getHeight());
if (reference == null) {
reference = bm;
} else {
BitmapUtils.compareBitmaps(bm, reference);
}
} catch (IOException e) {
fail("Decoding " + uri.toString() + " yielded " + e);
}
}
}
@Test(expected=IOException.class)
public void testZeroLengthByteBuffer() throws IOException {
Drawable drawable = ImageDecoder.decodeDrawable(
ImageDecoder.createSource(ByteBuffer.wrap(new byte[10], 0, 0)));
fail("should not have reached here!");
}
private interface ByteBufferSupplier extends Supplier<ByteBuffer> {};
@Test
public void testOffsetByteArray() {
for (Record record : RECORDS) {
int offset = 10;
int extra = 15;
byte[] array = getAsByteArray(record.resId, offset, extra);
int length = array.length - extra - offset;
// Used for SourceCreators that set both a position and an offset.
int myOffset = 3;
int myPosition = 7;
assertEquals(offset, myOffset + myPosition);
ByteBufferSupplier[] suppliers = new ByteBufferSupplier[] {
// Internally, this gives the buffer a position, but not an offset.
() -> ByteBuffer.wrap(array, offset, length),
// Same, but make it readOnly to ensure that we test the
// ByteBufferSource rather than the ByteArraySource.
() -> ByteBuffer.wrap(array, offset, length).asReadOnlyBuffer(),
() -> {
// slice() to give the buffer an offset.
ByteBuffer buf = ByteBuffer.wrap(array, 0, array.length - extra);
buf.position(offset);
return buf.slice();
},
() -> {
// Same, but make it readOnly to ensure that we test the
// ByteBufferSource rather than the ByteArraySource.
ByteBuffer buf = ByteBuffer.wrap(array, 0, array.length - extra);
buf.position(offset);
return buf.slice().asReadOnlyBuffer();
},
() -> {
// Use both a position and an offset.
ByteBuffer buf = ByteBuffer.wrap(array, myOffset,
array.length - extra - myOffset);
buf = buf.slice();
buf.position(myPosition);
return buf;
},
() -> {
// Same, as readOnly.
ByteBuffer buf = ByteBuffer.wrap(array, myOffset,
array.length - extra - myOffset);
buf = buf.slice();
buf.position(myPosition);
return buf.asReadOnlyBuffer();
},
() -> {
// Direct ByteBuffer with a position.
ByteBuffer buf = ByteBuffer.allocateDirect(array.length);
buf.put(array);
buf.position(offset);
return buf;
},
() -> {
// Sliced direct ByteBuffer, for an offset.
ByteBuffer buf = ByteBuffer.allocateDirect(array.length);
buf.put(array);
buf.position(offset);
return buf.slice();
},
() -> {
// Direct ByteBuffer with position and offset.
ByteBuffer buf = ByteBuffer.allocateDirect(array.length);
buf.put(array);
buf.position(myOffset);
buf = buf.slice();
buf.position(myPosition);
return buf;
},
};
for (int i = 0; i < suppliers.length; ++i) {
ByteBuffer buffer = suppliers[i].get();
final int position = buffer.position();
ImageDecoder.Source src = ImageDecoder.createSource(buffer);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src);
assertNotNull(drawable);
} catch (IOException e) {
fail("Failed with exception " + e);
}
assertEquals("Mismatch for supplier " + i,
position, buffer.position());
}
}
}
@Test
public void testResourceSource() {
for (Record record : RECORDS) {
ImageDecoder.Source src = ImageDecoder.createSource(mRes, record.resId);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src);
assertNotNull(drawable);
} catch (IOException e) {
fail("Failed " + getAsResourceUri(record.resId) + " with " + e);
}
}
}
private BitmapDrawable decodeBitmapDrawable(int resId) {
ImageDecoder.Source src = ImageDecoder.createSource(mRes, resId);
try {
Drawable drawable = ImageDecoder.decodeDrawable(src);
assertNotNull(drawable);
assertTrue(drawable instanceof BitmapDrawable);
return (BitmapDrawable) drawable;
} catch (IOException e) {
fail("Failed " + getAsResourceUri(resId) + " with " + e);
return null;
}
}
@Test
public void testUpscale() {
final int originalDensity = mRes.getDisplayMetrics().densityDpi;
try {
for (Record record : RECORDS) {
final int resId = record.resId;
// Set a high density. This will result in a larger drawable, but
// not a larger Bitmap.
mRes.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_XXXHIGH;
BitmapDrawable drawable = decodeBitmapDrawable(resId);
Bitmap bm = drawable.getBitmap();
assertEquals(record.width, bm.getWidth());
assertEquals(record.height, bm.getHeight());
assertTrue(drawable.getIntrinsicWidth() > record.width);
assertTrue(drawable.getIntrinsicHeight() > record.height);
// Set a low density. This will result in a smaller drawable and
// Bitmap, unless the true density is DENSITY_MEDIUM, which matches
// the density of the asset.
mRes.getDisplayMetrics().densityDpi = DisplayMetrics.DENSITY_LOW;
drawable = decodeBitmapDrawable(resId);
bm = drawable.getBitmap();
if (originalDensity == DisplayMetrics.DENSITY_MEDIUM) {
// Although we've modified |densityDpi|, ImageDecoder knows the
// true density matches the asset, so it will not downscale at
// decode time.
assertEquals(bm.getWidth(), record.width);
assertEquals(bm.getHeight(), record.height);
// The drawable should still be smaller.
assertTrue(bm.getWidth() > drawable.getIntrinsicWidth());
assertTrue(bm.getHeight() > drawable.getIntrinsicHeight());
} else {
// The bitmap is scaled down at decode time, so it matches the
// drawable size, and is smaller than the original.
assertTrue(bm.getWidth() < record.width);
assertTrue(bm.getHeight() < record.height);
assertEquals(bm.getWidth(), drawable.getIntrinsicWidth());
assertEquals(bm.getHeight(), drawable.getIntrinsicHeight());
}
}
} finally {
mRes.getDisplayMetrics().densityDpi = originalDensity;
}
}
private static class AssetRecord {
public final String name;
public final int width;
public final int height;
public final boolean isF16;
private final ColorSpace mColorSpace;
AssetRecord(String name, int width, int height, boolean isF16, ColorSpace colorSpace) {
this.name = name;
this.width = width;
this.height = height;
this.isF16 = isF16;
mColorSpace = colorSpace;
}
public void checkColorSpace(ColorSpace requested, ColorSpace actual) {
assertNotNull(actual);
if (this.isF16) {
assertSame(ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB), actual);
} else if (requested != null) {
assertSame(requested, actual);
} else if (mColorSpace == null) {
assertEquals(this.name, "Unknown", actual.getName());
} else {
assertSame(this.name, mColorSpace, actual);
}
}
}
private static final AssetRecord[] ASSETS = new AssetRecord[] {
// A null ColorSpace means that the color space is "Unknown".
new AssetRecord("almost-red-adobe.png", 1, 1, false, null),
new AssetRecord("green-p3.png", 64, 64, false,
ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
new AssetRecord("green-srgb.png", 64, 64, false, sSRGB),
new AssetRecord("prophoto-rgba16f.png", 64, 64, true,
ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB)),
new AssetRecord("purple-cmyk.png", 64, 64, false, sSRGB),
new AssetRecord("purple-displayprofile.png", 64, 64, false, null),
new AssetRecord("red-adobergb.png", 64, 64, false,
ColorSpace.get(ColorSpace.Named.ADOBE_RGB)),
new AssetRecord("translucent-green-p3.png", 64, 64, false,
ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
};
@Test
public void testAssetSource() {
AssetManager assets = mRes.getAssets();
for (AssetRecord record : ASSETS) {
ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
if (record.isF16) {
// CTS infrastructure fails to create F16 HARDWARE Bitmaps, so this
// switches to using software.
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
record.checkColorSpace(null, info.getColorSpace());
});
assertEquals(record.name, record.width, bm.getWidth());
assertEquals(record.name, record.height, bm.getHeight());
record.checkColorSpace(null, bm.getColorSpace());
} catch (IOException e) {
fail("Failed to decode asset " + record.name + " with " + e);
}
}
}
@Test
public void testTargetColorSpace() {
AssetManager assets = mRes.getAssets();
for (AssetRecord record : ASSETS) {
ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
for (ColorSpace cs : new ColorSpace[] {
sSRGB,
ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB),
ColorSpace.get(ColorSpace.Named.DISPLAY_P3),
ColorSpace.get(ColorSpace.Named.ADOBE_RGB),
ColorSpace.get(ColorSpace.Named.BT709),
ColorSpace.get(ColorSpace.Named.BT2020),
ColorSpace.get(ColorSpace.Named.DCI_P3),
ColorSpace.get(ColorSpace.Named.NTSC_1953),
ColorSpace.get(ColorSpace.Named.SMPTE_C),
ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB),
// FIXME: These will not match due to b/77276533.
// ColorSpace.get(ColorSpace.Named.LINEAR_SRGB),
// ColorSpace.get(ColorSpace.Named.ACES),
// ColorSpace.get(ColorSpace.Named.ACESCG),
}) {
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
if (record.isF16) {
// CTS infrastructure fails to create F16 HARDWARE Bitmaps, so this
// switches to using software.
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
}
decoder.setTargetColorSpace(cs);
});
record.checkColorSpace(cs, bm.getColorSpace());
} catch (IOException e) {
fail("Failed to decode asset " + record.name + " to " + cs + " with " + e);
}
}
}
}
@Test
public void testTargetColorSpaceNonRGB() {
ImageDecoder.Source src = mCreators[0].apply(R.drawable.png_test);
for (ColorSpace cs : new ColorSpace[] {
ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB),
ColorSpace.get(ColorSpace.Named.CIE_LAB),
ColorSpace.get(ColorSpace.Named.CIE_XYZ),
}) {
try {
ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setTargetColorSpace(cs);
});
fail("Should have thrown an IllegalArgumentException for setTargetColorSpace("
+ cs + ")!");
} catch (IOException e) {
fail("Failed to decode png_test with " + e);
} catch (IllegalArgumentException illegal) {
// This is expected.
}
}
}
private Bitmap drawToBitmap(Drawable dr) {
Bitmap bm = Bitmap.createBitmap(dr.getIntrinsicWidth(), dr.getIntrinsicHeight(),
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bm);
dr.draw(canvas);
return bm;
}
private void testReuse(ImageDecoder.Source src, String name) {
Drawable first = null;
try {
first = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
} catch (IOException e) {
fail("Failed on first decode of " + name + " using " + src + "!");
}
Drawable second = null;
try {
second = ImageDecoder.decodeDrawable(src, (decoder, info, s) -> {
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
});
} catch (IOException e) {
fail("Failed on second decode of " + name + " using " + src + "!");
}
assertEquals(first.getIntrinsicWidth(), second.getIntrinsicWidth());
assertEquals(first.getIntrinsicHeight(), second.getIntrinsicHeight());
Bitmap bm1 = drawToBitmap(first);
Bitmap bm2 = drawToBitmap(second);
BitmapUtils.compareBitmaps(bm1, bm2);
}
@Test
@LargeTest
public void testReuse() {
for (Record record : RECORDS) {
String name = getAsResourceUri(record.resId).toString();
for (SourceCreator f : mCreators) {
ImageDecoder.Source src = f.apply(record.resId);
testReuse(src, name);
}
{
ImageDecoder.Source src = ImageDecoder.createSource(mRes, record.resId);
testReuse(src, name);
}
for (UriCreator f : mUriCreators) {
Uri uri = f.apply(record.resId);
ImageDecoder.Source src = ImageDecoder.createSource(mContentResolver, uri);
testReuse(src, uri.toString());
}
{
ImageDecoder.Source src = ImageDecoder.createSource(getAsFile(record.resId));
testReuse(src, name);
}
}
AssetManager assets = mRes.getAssets();
for (AssetRecord record : ASSETS) {
ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
testReuse(src, record.name);
}
ImageDecoder.Source src = mCreators[0].apply(R.drawable.animated);
testReuse(src, "animated.gif");
}
}