Add tests for AImageDecoder
Bug: 135133301
Bug: 139124386
Test: This
Add AImageDecoderTest.java and its native counterpart for testing the
APIs of AImageDecoder.
Add Utils.java for sharing code between different tests.
Move BitmapFactoryTest#obtainPath into Utils.java
ImageDecoderTest:
- make various methods and classes package-private so they can be used
by AImageDecoderTest.
- Add Record.isGray and (Asset)Record.hasAlpha and a grayscale Record to
exercise AImageDecoder features.
- Move getAsResourceUri into Utils.
Change-Id: Ib84462ea5fa8a7779eaa44494775e182e52ecaca
diff --git a/tests/tests/graphics/jni/Android.bp b/tests/tests/graphics/jni/Android.bp
index 305ced3..0a302de 100644
--- a/tests/tests/graphics/jni/Android.bp
+++ b/tests/tests/graphics/jni/Android.bp
@@ -17,6 +17,7 @@
gtest: false,
srcs: [
"CtsGraphicsJniOnLoad.cpp",
+ "android_graphics_cts_AImageDecoderTest.cpp",
"android_graphics_cts_ANativeWindowTest.cpp",
"android_graphics_cts_ASurfaceTextureTest.cpp",
"android_graphics_cts_BasicVulkanGpuTest.cpp",
diff --git a/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp b/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp
index db7919d..1d7dd89 100644
--- a/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp
+++ b/tests/tests/graphics/jni/CtsGraphicsJniOnLoad.cpp
@@ -17,6 +17,7 @@
#include <jni.h>
#include <stdio.h>
+extern int register_android_graphics_cts_AImageDecoderTest(JNIEnv*);
extern int register_android_graphics_cts_ANativeWindowTest(JNIEnv*);
extern int register_android_graphics_cts_ASurfaceTextureTest(JNIEnv*);
extern int register_android_graphics_cts_BasicVulkanGpuTest(JNIEnv*);
@@ -33,6 +34,8 @@
JNIEnv* env = nullptr;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK)
return JNI_ERR;
+ if (register_android_graphics_cts_AImageDecoderTest(env))
+ return JNI_ERR;
if (register_android_graphics_cts_ANativeWindowTest(env))
return JNI_ERR;
if (register_android_graphics_cts_ASurfaceTextureTest(env))
diff --git a/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp b/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
new file mode 100644
index 0000000..01da233
--- /dev/null
+++ b/tests/tests/graphics/jni/android_graphics_cts_AImageDecoderTest.cpp
@@ -0,0 +1,1059 @@
+/*
+ * Copyright 2019 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.
+ *
+ */
+
+#define LOG_TAG "AImageDecoderTest"
+
+#include <jni.h>
+#include <android/asset_manager.h>
+#include <android/asset_manager_jni.h>
+#include <android/bitmap.h>
+#include <android/imagedecoder.h>
+#include <android/rect.h>
+
+#include "NativeTestHelpers.h"
+
+#include <cstdlib>
+#include <cstring>
+#include <initializer_list>
+#include <limits>
+#include <memory>
+#include <stdio.h>
+#include <unistd.h>
+
+#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
+
+using AssetCloser = std::unique_ptr<AAsset, decltype(&AAsset_close)>;
+using DecoderDeleter = std::unique_ptr<AImageDecoder, decltype(&AImageDecoder_delete)>;
+
+static void testEmptyCreate(JNIEnv* env, jclass) {
+ AImageDecoder* decoderPtr = nullptr;
+ for (AImageDecoder** outDecoder : { &decoderPtr, (AImageDecoder**) nullptr }) {
+ for (AAsset* asset : { nullptr }) {
+ int result = AImageDecoder_createFromAAsset(asset, outDecoder);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ if (outDecoder) {
+ ASSERT_EQ(nullptr, *outDecoder);
+ }
+ }
+
+ for (int fd : { 0, -1 }) {
+ int result = AImageDecoder_createFromFd(fd, outDecoder);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ if (outDecoder) {
+ ASSERT_EQ(nullptr, *outDecoder);
+ }
+ }
+
+ auto testEmptyBuffer = [env, outDecoder](void* buffer, size_t length) {
+ int result = AImageDecoder_createFromBuffer(buffer, length, outDecoder);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ if (outDecoder) {
+ ASSERT_EQ(nullptr, *outDecoder);
+ }
+ };
+ testEmptyBuffer(nullptr, 0);
+ char buf[4];
+ testEmptyBuffer(buf, 0);
+ }
+}
+
+static AAsset* openAsset(JNIEnv* env, jobject jAssets, jstring jFile, int mode) {
+ AAssetManager* nativeManager = AAssetManager_fromJava(env, jAssets);
+ const char* file = env->GetStringUTFChars(jFile, nullptr);
+ AAsset* asset = AAssetManager_open(nativeManager, file, mode);
+ if (!asset) {
+ ALOGE("Could not open %s", file);
+ } else {
+ ALOGD("Testing %s", file);
+ }
+ env->ReleaseStringUTFChars(jFile, file);
+ return asset;
+}
+
+static void testNullDecoder(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+ AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_BUFFER);
+ ASSERT_NE(asset, nullptr);
+ AssetCloser assetCloser(asset, AAsset_close);
+
+ {
+ int result = AImageDecoder_createFromAAsset(asset, nullptr);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+
+ {
+ const void* buffer = AAsset_getBuffer(asset);
+ ASSERT_NE(buffer, nullptr);
+
+ int result = AImageDecoder_createFromBuffer(buffer, AAsset_getLength(asset), nullptr);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+
+ {
+ off_t start, length;
+ int fd = AAsset_openFileDescriptor(asset, &start, &length);
+ ASSERT_GT(fd, 0);
+
+ off_t offset = ::lseek(fd, start, SEEK_SET);
+ ASSERT_EQ(start, offset);
+
+ int result = AImageDecoder_createFromFd(fd, nullptr);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ close(fd);
+ }
+
+ {
+ auto stride = AImageDecoder_getMinimumStride(nullptr);
+ ASSERT_EQ(0, stride);
+ }
+
+ {
+ char buf[4];
+ int result = AImageDecoder_decodeImage(nullptr, buf, 4, 4);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+
+ {
+ int result = AImageDecoder_setAndroidBitmapFormat(nullptr, ANDROID_BITMAP_FORMAT_RGBA_8888);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ auto format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(nullptr);
+ ASSERT_EQ(ANDROID_BITMAP_FORMAT_NONE, format);
+ }
+
+ {
+ int result = AImageDecoder_setAlphaFlags(nullptr, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ int alpha = AImageDecoderHeaderInfo_getAlphaFlags(nullptr);
+ // FIXME: Better invalid value?
+ ASSERT_EQ(-1, alpha);
+ }
+
+ ASSERT_EQ(0, AImageDecoderHeaderInfo_getWidth(nullptr));
+ ASSERT_EQ(0, AImageDecoderHeaderInfo_getHeight(nullptr));
+ ASSERT_EQ(nullptr, AImageDecoderHeaderInfo_getMimeType(nullptr));
+ ASSERT_EQ(false, AImageDecoderHeaderInfo_isAnimated(nullptr));
+
+ {
+ int result = AImageDecoder_setTargetSize(nullptr, 1, 1);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+ {
+ ARect rect {0, 0, 10, 10};
+ int result = AImageDecoder_setCrop(nullptr, rect);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+}
+
+static void testInfo(JNIEnv* env, jclass, jlong imageDecoderPtr, jint width, jint height,
+ jstring jMimeType, jboolean isAnimated, jboolean isF16) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ ASSERT_NE(decoder, nullptr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+ int32_t actualWidth = AImageDecoderHeaderInfo_getWidth(info);
+ ASSERT_EQ(width, actualWidth);
+ int32_t actualHeight = AImageDecoderHeaderInfo_getHeight(info);
+ ASSERT_EQ(height, actualHeight);
+
+ const char* mimeType = env->GetStringUTFChars(jMimeType, nullptr);
+ ASSERT_NE(mimeType, nullptr);
+ ASSERT_EQ(0, strcmp(mimeType, AImageDecoderHeaderInfo_getMimeType(info)));
+ env->ReleaseStringUTFChars(jMimeType, mimeType);
+ ASSERT_EQ(isAnimated, AImageDecoderHeaderInfo_isAnimated(info));
+ auto format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+ if (isF16) {
+ ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_F16, format);
+ } else {
+ ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_8888, format);
+ }
+}
+
+static jlong openAssetNative(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+ // FIXME: Test the other modes? Or more to the point, pass in the mode? It
+ // seems that when we want a buffer we should use AASSET_MODE_BUFFER.
+ AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+ if (!asset) {
+ fail(env, "Failed to open native asset!");
+ }
+ return reinterpret_cast<jlong>(asset);
+}
+
+static void closeAsset(JNIEnv*, jclass, jlong asset) {
+ AAsset_close(reinterpret_cast<AAsset*>(asset));
+}
+
+static jlong createFromAsset(JNIEnv* env, jclass, jlong asset) {
+ AImageDecoder* decoder = nullptr;
+ int result = AImageDecoder_createFromAAsset(reinterpret_cast<AAsset*>(asset), &decoder);
+ if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+ fail(env, "Failed to create AImageDecoder!");
+ }
+ return reinterpret_cast<jlong>(decoder);
+}
+
+static jlong createFromFd(JNIEnv* env, jclass, int fd) {
+ AImageDecoder* decoder = nullptr;
+ int result = AImageDecoder_createFromFd(fd, &decoder);
+ if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+ fail(env, "Failed to create AImageDecoder!");
+ }
+ return reinterpret_cast<jlong>(decoder);
+}
+
+static jlong createFromAssetFd(JNIEnv* env, jclass, jlong assetPtr) {
+ AAsset* asset = reinterpret_cast<AAsset*>(assetPtr);
+ off_t start, length;
+ int fd = AAsset_openFileDescriptor(asset, &start, &length);
+ if (fd <= 0) {
+ fail(env, "Failed to open file descriptor!");
+ return -1;
+ }
+
+ off_t offset = ::lseek(fd, start, SEEK_SET);
+ if (offset != start) {
+ fail(env, "Failed to seek file descriptor!");
+ return -1;
+ }
+
+ return createFromFd(env, nullptr, fd);
+}
+
+static jlong createFromAssetBuffer(JNIEnv* env, jclass, jlong assetPtr) {
+ AAsset* asset = reinterpret_cast<AAsset*>(assetPtr);
+ const void* buffer = AAsset_getBuffer(asset);
+ if (!buffer) {
+ fail(env, "AAsset_getBuffer failed!");
+ return -1;
+ }
+
+ AImageDecoder* decoder = nullptr;
+ int result = AImageDecoder_createFromBuffer(buffer, AAsset_getLength(asset), &decoder);
+ if (ANDROID_IMAGE_DECODER_SUCCESS != result || !decoder) {
+ fail(env, "AImageDecoder_createFromBuffer failed!");
+ return -1;
+ }
+ return reinterpret_cast<jlong>(decoder);
+}
+
+static void testCreateIncomplete(JNIEnv* env, jclass, jobject jAssets, jstring jFile,
+ jint truncatedLength) {
+ AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+ ASSERT_NE(asset, nullptr);
+ AssetCloser assetCloser(asset, AAsset_close);
+
+ const void* buffer = AAsset_getBuffer(asset);
+ ASSERT_NE(buffer, nullptr);
+
+ AImageDecoder* decoder;
+ int result = AImageDecoder_createFromBuffer(buffer, truncatedLength, &decoder);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INCOMPLETE, result);
+ ASSERT_EQ(decoder, nullptr);
+}
+
+static void testCreateUnsupported(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+ AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+ ASSERT_NE(asset, nullptr);
+ AssetCloser assetCloser(asset, AAsset_close);
+
+ AImageDecoder* decoder;
+ int result = AImageDecoder_createFromAAsset(asset, &decoder);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_UNSUPPORTED_FORMAT, result);
+ ASSERT_EQ(decoder, nullptr);
+}
+
+static void testSetFormat(JNIEnv* env, jclass, jlong imageDecoderPtr,
+ jboolean isF16, jboolean isGray) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ // Store the format so we can ensure that it doesn't change when we call
+ // AImageDecoder_setAndroidBitmapFormat.
+ const auto format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+ if (isF16) {
+ ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_F16, format);
+ } else {
+ ASSERT_EQ(ANDROID_BITMAP_FORMAT_RGBA_8888, format);
+ }
+
+ int result = AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_A_8);
+ if (isGray) {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ } else {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+ }
+ ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+
+ result = AImageDecoder_setAndroidBitmapFormat(decoder, ANDROID_BITMAP_FORMAT_RGB_565);
+ int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+ if (alpha == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ } else {
+ ASSERT_EQ(ANDROID_BITMAP_FLAGS_ALPHA_PREMUL, alpha);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+ }
+ ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+
+ for (auto newFormat : { ANDROID_BITMAP_FORMAT_RGBA_4444, ANDROID_BITMAP_FORMAT_NONE }) {
+ result = AImageDecoder_setAndroidBitmapFormat(decoder, newFormat);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+ ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+ }
+
+ for (auto newFormat : { ANDROID_BITMAP_FORMAT_RGBA_8888, ANDROID_BITMAP_FORMAT_RGBA_F16 }) {
+ result = AImageDecoder_setAndroidBitmapFormat(decoder, newFormat);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+ }
+
+ for (auto invalidFormat : { -1, 42, 67 }) {
+ result = AImageDecoder_setAndroidBitmapFormat(decoder, invalidFormat);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ ASSERT_EQ(format, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+ }
+}
+
+static void testSetAlpha(JNIEnv* env, jclass, jlong imageDecoderPtr, jboolean hasAlpha) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ // Store the alpha so we can ensure that it doesn't change when we call
+ // AImageDecoder_setAlphaFlags.
+ const int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+ if (hasAlpha) {
+ ASSERT_EQ(ANDROID_BITMAP_FLAGS_ALPHA_PREMUL, alpha);
+ } else {
+ ASSERT_EQ(ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE, alpha);
+ }
+
+ int result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE);
+ if (hasAlpha) {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+ } else {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ }
+ ASSERT_EQ(alpha, AImageDecoderHeaderInfo_getAlphaFlags(info));
+
+ for (int newAlpha : { ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL }){
+ result = AImageDecoder_setAlphaFlags(decoder, newAlpha);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ ASSERT_EQ(alpha, AImageDecoderHeaderInfo_getAlphaFlags(info));
+ }
+
+ for (int invalidAlpha : std::initializer_list<int>{
+ ANDROID_BITMAP_FLAGS_ALPHA_MASK, -1, 3, 5, 16 }) {
+ result = AImageDecoder_setAlphaFlags(decoder, invalidAlpha);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ ASSERT_EQ(alpha, AImageDecoderHeaderInfo_getAlphaFlags(info));
+ }
+}
+
+static int bytesPerPixel(AndroidBitmapFormat format) {
+ switch (format) {
+ case ANDROID_BITMAP_FORMAT_RGBA_8888:
+ return 4;
+ case ANDROID_BITMAP_FORMAT_RGB_565:
+ return 2;
+ case ANDROID_BITMAP_FORMAT_A_8:
+ return 1;
+ case ANDROID_BITMAP_FORMAT_RGBA_F16:
+ return 8;
+ case ANDROID_BITMAP_FORMAT_NONE:
+ case ANDROID_BITMAP_FORMAT_RGBA_4444:
+ return 0;
+ }
+}
+
+static void testGetMinimumStride(JNIEnv* env, jclass, jlong imageDecoderPtr,
+ jboolean isF16, jboolean isGray) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+ size_t stride = AImageDecoder_getMinimumStride(decoder);
+
+ if (isF16) {
+ ASSERT_EQ(bytesPerPixel(ANDROID_BITMAP_FORMAT_RGBA_F16) * width, stride);
+ } else {
+ ASSERT_EQ(bytesPerPixel(ANDROID_BITMAP_FORMAT_RGBA_8888) * width, stride);
+ }
+
+ auto setFormatAndCheckStride = [env, decoder, width, &stride](AndroidBitmapFormat format) {
+ int result = AImageDecoder_setAndroidBitmapFormat(decoder, format);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ stride = AImageDecoder_getMinimumStride(decoder);
+ ASSERT_EQ(bytesPerPixel(format) * width, stride);
+ };
+
+ int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+ if (alpha == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+ setFormatAndCheckStride(ANDROID_BITMAP_FORMAT_RGB_565);
+ }
+
+ if (isGray) {
+ setFormatAndCheckStride(ANDROID_BITMAP_FORMAT_A_8);
+ }
+
+ for (auto newFormat : { ANDROID_BITMAP_FORMAT_RGBA_8888, ANDROID_BITMAP_FORMAT_RGBA_F16 }) {
+ setFormatAndCheckStride(newFormat);
+ }
+
+ for (auto badFormat : { ANDROID_BITMAP_FORMAT_RGBA_4444, ANDROID_BITMAP_FORMAT_NONE }) {
+ int result = AImageDecoder_setAndroidBitmapFormat(decoder, badFormat);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+
+ // Stride is unchanged.
+ ASSERT_EQ(stride, AImageDecoder_getMinimumStride(decoder));
+ }
+}
+
+static bool bitmapsEqual(size_t minStride, int height,
+ void* pixelsA, size_t strideA,
+ void* pixelsB, size_t strideB) {
+ for (int y = 0; y < height; ++y) {
+ auto* rowA = reinterpret_cast<char*>(pixelsA) + strideA * y;
+ auto* rowB = reinterpret_cast<char*>(pixelsB) + strideB * y;
+ if (memcmp(rowA, rowB, minStride) != 0) {
+ ALOGE("Bitmap mismatch on line %i", y);
+ return false;
+ }
+ }
+ return true;
+}
+
+#define EXPECT_EQ(msg, a, b) \
+ if ((a) != (b)) { \
+ ALOGE(msg); \
+ return false; \
+ }
+
+#define EXPECT_GE(msg, a, b) \
+ if ((a) < (b)) { \
+ ALOGE(msg); \
+ return false; \
+ }
+
+static bool bitmapsEqual(JNIEnv* env, jobject jbitmap, AndroidBitmapFormat format,
+ int width, int height, int alphaFlags, size_t minStride,
+ void* pixelsA, size_t strideA) {
+ AndroidBitmapInfo jInfo;
+ int bitmapResult = AndroidBitmap_getInfo(env, jbitmap, &jInfo);
+ EXPECT_EQ("Failed to getInfo on Bitmap", ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+ EXPECT_EQ("Wrong format", jInfo.format, format);
+
+ // If the image is truly opaque, the Java Bitmap will report OPAQUE, even if
+ // the AImageDecoder requested PREMUL/UNPREMUL. In that case, it is okay for
+ // the two to disagree. We must ensure that we don't end up with one PREMUL
+ // and the other UNPREMUL, though.
+ auto jAlphaFlags = jInfo.flags & ANDROID_BITMAP_FLAGS_ALPHA_MASK;
+ if (jAlphaFlags != ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+ EXPECT_EQ("Wrong alpha type", jAlphaFlags, alphaFlags);
+ }
+
+ EXPECT_EQ("Wrong width", jInfo.width, width);
+ EXPECT_EQ("Wrong height", jInfo.height, height);
+
+ EXPECT_GE("Stride too small", jInfo.stride, minStride);
+
+ void* jPixels;
+ bitmapResult = AndroidBitmap_lockPixels(env, jbitmap, &jPixels);
+ EXPECT_EQ("Failed to lockPixels", ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+ bool equal = bitmapsEqual(minStride, height, pixelsA, strideA, jPixels, jInfo.stride);
+
+ bitmapResult = AndroidBitmap_unlockPixels(env, jbitmap);
+ EXPECT_EQ("Failed to unlockPixels", ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+ return equal;
+}
+
+static void testDecode(JNIEnv* env, jclass, jlong imageDecoderPtr,
+ jint androidBitmapFormat, jboolean unpremul, jobject jbitmap) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ int result;
+ int alphaFlags = AImageDecoderHeaderInfo_getAlphaFlags(info);
+ if (androidBitmapFormat == ANDROID_BITMAP_FORMAT_NONE) {
+ androidBitmapFormat = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+ } else {
+ result = AImageDecoder_setAndroidBitmapFormat(decoder, androidBitmapFormat);
+ if (androidBitmapFormat == ANDROID_BITMAP_FORMAT_RGB_565) {
+ if (alphaFlags != ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+
+ // The caller only passes down the Bitmap if it is opaque.
+ ASSERT_EQ(nullptr, jbitmap);
+ return;
+ }
+ }
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ }
+
+ if (unpremul) {
+ result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ alphaFlags = ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL;
+ }
+
+ const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+ const int32_t height = AImageDecoderHeaderInfo_getHeight(info);
+ size_t minStride = AImageDecoder_getMinimumStride(decoder);
+
+ size_t size = minStride * height;
+ void* pixels = malloc(size);
+
+ {
+ // Try some invalid parameters.
+ result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride, size - minStride);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, 0, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ ASSERT_NE(jbitmap, nullptr);
+ ASSERT_TRUE(bitmapsEqual(env, jbitmap, (AndroidBitmapFormat) androidBitmapFormat,
+ width, height, alphaFlags, minStride, pixels, minStride));
+
+ // Used for subsequent decodes, to ensure they are identical to the
+ // original. For opaque images, this verifies that using PREMUL or UNPREMUL
+ // look the same. For all images, this verifies that decodeImage can be
+ // called multiple times.
+ auto decodeAgain = [=](int alpha) {
+ int r = AImageDecoder_setAlphaFlags(decoder, alpha);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, r);
+
+ void* otherPixels = malloc(size);
+ r = AImageDecoder_decodeImage(decoder, otherPixels, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, r);
+
+ ASSERT_TRUE(bitmapsEqual(minStride, height, pixels, minStride, otherPixels, minStride));
+ free(otherPixels);
+ };
+ if (alphaFlags == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+ for (int otherAlpha : { ANDROID_BITMAP_FLAGS_ALPHA_PREMUL,
+ ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL }) {
+ decodeAgain(otherAlpha);
+ }
+ } else {
+ decodeAgain(alphaFlags);
+ }
+
+ free(pixels);
+}
+
+static void testDecodeStride(JNIEnv* env, jclass, jlong imageDecoderPtr) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ const int height = AImageDecoderHeaderInfo_getHeight(info);
+ size_t minStride = AImageDecoder_getMinimumStride(decoder);
+
+ void* pixels = nullptr;
+
+ // The code in this loop relies on minStride being used first.
+ for (size_t stride : { minStride, (size_t) (minStride * 1.5), minStride * 3 }) {
+ size_t size = stride * (height - 1) + minStride;
+ void* decodePixels = malloc(size);
+ int result = AImageDecoder_decodeImage(decoder, decodePixels, stride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ if (pixels == nullptr) {
+ pixels = decodePixels;
+ } else {
+ ASSERT_TRUE(bitmapsEqual(minStride, height, pixels, minStride, decodePixels, stride));
+ free(decodePixels);
+ }
+ }
+
+ free(pixels);
+}
+
+static void testSetTargetSize(JNIEnv* env, jclass, jlong imageDecoderPtr) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const size_t defaultStride = AImageDecoder_getMinimumStride(decoder);
+
+ for (int width : { -1, 0, -500 }) {
+ int result = AImageDecoder_setTargetSize(decoder, width, 100);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+ // stride is unchanged, as the target size did not change.
+ ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+ }
+
+ for (int height : { -1, 0, -300 }) {
+ int result = AImageDecoder_setTargetSize(decoder, 100, height);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+ // stride is unchanged, as the target size did not change.
+ ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+ }
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+ const int bpp = bytesPerPixel(AImageDecoderHeaderInfo_getAndroidBitmapFormat(info));
+
+ for (int width : { 7, 100, 275, 300 }) {
+ int result = AImageDecoder_setTargetSize(decoder, width, 100);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+ size_t expectedStride = bpp * width;
+ ASSERT_EQ(expectedStride, actualStride);
+ }
+
+ // Verify that setting a large enough width to overflow 31 bits fails.
+ constexpr auto kMaxInt32 = std::numeric_limits<int32_t>::max();
+ int32_t maxWidth = kMaxInt32 / bpp;
+ for (int32_t width : { maxWidth / 2, maxWidth - 1, maxWidth }) {
+ int result = AImageDecoder_setTargetSize(decoder, width, 1);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+ size_t expectedStride = bpp * width;
+ ASSERT_EQ(expectedStride, actualStride);
+ }
+
+ for (int32_t width : { maxWidth + 1, (int32_t) (maxWidth * 1.5) }) {
+ int result = AImageDecoder_setTargetSize(decoder, width, 1);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+ }
+
+ // A height that results in overflowing 31 bits also fails.
+ int32_t maxHeight = kMaxInt32 / defaultStride;
+ const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+ for (int32_t height : { maxHeight / 2, maxHeight - 1, maxHeight }) {
+ int result = AImageDecoder_setTargetSize(decoder, width, height);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+ size_t expectedStride = bpp * width;
+ ASSERT_EQ(expectedStride, actualStride);
+ }
+
+ for (int32_t height : { maxHeight + 1, (int32_t) (maxHeight * 1.5) }) {
+ int result = AImageDecoder_setTargetSize(decoder, width, height);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+ }
+}
+
+static void testDecodeScaled(JNIEnv* env, jclass, jlong imageDecoderPtr,
+ jobject jbitmap) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ AndroidBitmapInfo jInfo;
+ int bitmapResult = AndroidBitmap_getInfo(env, jbitmap, &jInfo);
+ ASSERT_EQ(ANDROID_BITMAP_RESULT_SUCCESS, bitmapResult);
+
+ int result = AImageDecoder_setTargetSize(decoder, jInfo.width, jInfo.height);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ size_t minStride = AImageDecoder_getMinimumStride(decoder);
+ size_t size = minStride * jInfo.height;
+ void* pixels = malloc(size);
+
+ {
+ // Try some invalid parameters.
+ result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride, size - minStride);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ ASSERT_NE(jbitmap, nullptr);
+ ASSERT_TRUE(bitmapsEqual(env, jbitmap, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info),
+ jInfo.width, jInfo.height, AImageDecoderHeaderInfo_getAlphaFlags(info),
+ minStride, pixels, minStride));
+
+ // Verify that larger strides still behave as expected.
+ for (size_t stride : { (size_t) (minStride * 1.5), minStride * 3 }) {
+ size_t size = stride * (jInfo.height - 1) + minStride;
+ void* decodePixels = malloc(size);
+ result = AImageDecoder_decodeImage(decoder, decodePixels, stride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ ASSERT_TRUE(bitmapsEqual(minStride, jInfo.height, pixels, minStride, decodePixels, stride));
+ free(decodePixels);
+ }
+
+ free(pixels);
+}
+
+static void testSetCrop(JNIEnv* env, jclass, jobject jAssets, jstring jFile) {
+ AAsset* asset = openAsset(env, jAssets, jFile, AASSET_MODE_UNKNOWN);
+ ASSERT_NE(asset, nullptr);
+ AssetCloser assetCloser(asset, AAsset_close);
+
+ AImageDecoder* decoder;
+ int result = AImageDecoder_createFromAAsset(asset, &decoder);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ ASSERT_NE(decoder, nullptr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+ const int32_t height = AImageDecoderHeaderInfo_getHeight(info);
+ const AndroidBitmapFormat format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(info);
+ const size_t defaultStride = AImageDecoder_getMinimumStride(decoder);
+
+ if (width == 1 && height == 1) {
+ // The more general crop tests do not map well to this image. Test 1 x 1
+ // specifically.
+ for (ARect invalidCrop : std::initializer_list<ARect> {
+ { -1, 0, width, height },
+ { 0, -1, width, height },
+ { width, 0, 2 * width, height },
+ { 0, height, width, 2 * height },
+ { 1, 0, width + 1, height },
+ { 0, 1, width, height + 1 },
+ { 0, 0, 0, height },
+ { 0, 0, width, 0 },
+ }) {
+ int result = AImageDecoder_setCrop(decoder, invalidCrop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+ }
+ return;
+ }
+
+ for (ARect invalidCrop : std::initializer_list<ARect> {
+ { -1, 0, width, height },
+ { 0, -1, width, height },
+ { width, 0, 2 * width, height },
+ { 0, height, width, 2 * height },
+ { 1, 0, width + 1, height },
+ { 0, 1, width, height + 1 },
+ { width - 1, 0, 1, height },
+ { 0, height - 1, width, 1 },
+ { 0, 0, 0, height },
+ { 0, 0, width, 0 },
+ { 1, 1, 1, 1 },
+ { width, height, 0, 0 },
+ }) {
+ int result = AImageDecoder_setCrop(decoder, invalidCrop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+ }
+
+ for (ARect validCrop : std::initializer_list<ARect> {
+ { 0, 0, width, height },
+ { 0, 0, width / 2, height / 2},
+ { 0, 0, width / 3, height },
+ { 0, 0, width, height / 4},
+ { width / 2, 0, width, height / 2},
+ { 0, height / 2, width / 2, height },
+ { width / 2, height / 2, width, height },
+ { 1, 1, width - 1, height - 1 },
+ }) {
+ int result = AImageDecoder_setCrop(decoder, validCrop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+ size_t expectedStride = bytesPerPixel(format) * (validCrop.right - validCrop.left);
+ ASSERT_EQ(expectedStride, actualStride);
+ }
+
+ // Reset the crop, so we can test setting a crop *after* changing the
+ // target size.
+ result = AImageDecoder_setCrop(decoder, { 0, 0, 0, 0 });
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+
+ int newWidth = width / 2, newHeight = height / 2;
+ result = AImageDecoder_setTargetSize(decoder, newWidth, newHeight);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ const size_t halfStride = AImageDecoder_getMinimumStride(decoder);
+ {
+ size_t expectedStride = bytesPerPixel(format) * newWidth;
+ ASSERT_EQ(expectedStride, halfStride);
+ }
+
+ // At the smaller target size, crops that were previously valid no longer
+ // are.
+ for (ARect invalidCrop : std::initializer_list<ARect> {
+ { 0, 0, width / 3, height },
+ { 0, 0, width, height / 4},
+ { width / 2, 0, width, height / 2},
+ { 0, height / 2, width / 2, height },
+ { width / 2, height / 2, width, height },
+ { 1, 1, width - 1, height - 1 },
+ }) {
+ int result = AImageDecoder_setCrop(decoder, invalidCrop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ ASSERT_EQ(halfStride, AImageDecoder_getMinimumStride(decoder));
+ }
+
+ for (ARect validCrop : std::initializer_list<ARect> {
+ { 0, 0, newWidth, newHeight },
+ { 0, 0, newWidth / 3, newHeight },
+ { 0, 0, newWidth, newHeight / 4},
+ { newWidth / 2, 0, newWidth, newHeight / 2},
+ { 0, newHeight / 2, newWidth / 2, newHeight },
+ { newWidth / 2, newHeight / 2, newWidth, newHeight },
+ { 1, 1, newWidth - 1, newHeight - 1 },
+ }) {
+ int result = AImageDecoder_setCrop(decoder, validCrop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+ size_t expectedStride = bytesPerPixel(format) * (validCrop.right - validCrop.left);
+ ASSERT_EQ(expectedStride, actualStride);
+ }
+
+ newWidth = width * 2;
+ newHeight = height * 2;
+ result = AImageDecoder_setTargetSize(decoder, newWidth, newHeight);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ for (ARect validCrop : std::initializer_list<ARect> {
+ { width, 0, newWidth, height },
+ { 0, height * 3 / 4, width * 4 / 3, height }
+ }) {
+ int result = AImageDecoder_setCrop(decoder, validCrop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ size_t actualStride = AImageDecoder_getMinimumStride(decoder);
+ size_t expectedStride = bytesPerPixel(format) * (validCrop.right - validCrop.left);
+ ASSERT_EQ(expectedStride, actualStride);
+ }
+
+ // Reset crop and target size, so that we can verify that setting a crop and
+ // then setting target size that will not support the crop fails.
+ result = AImageDecoder_setCrop(decoder, { 0, 0, 0, 0 });
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ result = AImageDecoder_setTargetSize(decoder, width, height);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ ASSERT_EQ(defaultStride, AImageDecoder_getMinimumStride(decoder));
+
+ ARect crop{ width / 2, height / 2, width, height };
+ result = AImageDecoder_setCrop(decoder, crop);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ const size_t croppedStride = AImageDecoder_getMinimumStride(decoder);
+ {
+ size_t expectedStride = bytesPerPixel(format) * (crop.right - crop.left);
+ ASSERT_EQ(expectedStride, croppedStride);
+ }
+ result = AImageDecoder_setTargetSize(decoder, width / 2, height / 2);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+ ASSERT_EQ(croppedStride, AImageDecoder_getMinimumStride(decoder));
+}
+
+static void testDecodeCrop(JNIEnv* env, jclass, jlong imageDecoderPtr,
+ jobject jbitmap, jint targetWidth, jint targetHeight,
+ jint left, jint top, jint right, jint bottom) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ int result;
+ if (targetWidth != 0 && targetHeight != 0) {
+ result = AImageDecoder_setTargetSize(decoder, targetWidth, targetHeight);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ }
+
+ ARect rect { left, top, right, bottom };
+ result = AImageDecoder_setCrop(decoder, rect);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ const int32_t width = right - left;
+ const int32_t height = bottom - top;
+ size_t minStride = AImageDecoder_getMinimumStride(decoder);
+ size_t size = minStride * height;
+ void* pixels = malloc(size);
+
+ {
+ // Try some invalid parameters.
+ result = AImageDecoder_decodeImage(decoder, nullptr, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride - 1, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride, size - minStride);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_BAD_PARAMETER, result);
+ }
+
+ result = AImageDecoder_decodeImage(decoder, pixels, minStride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(info, nullptr);
+
+ ASSERT_NE(jbitmap, nullptr);
+ ASSERT_TRUE(bitmapsEqual(env, jbitmap, AImageDecoderHeaderInfo_getAndroidBitmapFormat(info),
+ width, height, AImageDecoderHeaderInfo_getAlphaFlags(info),
+ minStride, pixels, minStride));
+
+ // Verify that larger strides still behave as expected.
+ for (size_t stride : { (size_t) (minStride * 1.5), minStride * 3 }) {
+ size_t size = stride * (height - 1) + minStride;
+ void* decodePixels = malloc(size);
+ result = AImageDecoder_decodeImage(decoder, decodePixels, stride, size);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ ASSERT_TRUE(bitmapsEqual(minStride, height, pixels, minStride, decodePixels, stride));
+ free(decodePixels);
+ }
+
+ free(pixels);
+}
+
+static void testScalePlusUnpremul(JNIEnv* env, jclass, jlong imageDecoderPtr) {
+ AImageDecoder* decoder = reinterpret_cast<AImageDecoder*>(imageDecoderPtr);
+ DecoderDeleter decoderDeleter(decoder, AImageDecoder_delete);
+
+ const AImageDecoderHeaderInfo* info = AImageDecoder_getHeaderInfo(decoder);
+ ASSERT_NE(nullptr, info);
+
+ const int32_t width = AImageDecoderHeaderInfo_getWidth(info);
+ const int32_t height = AImageDecoderHeaderInfo_getHeight(info);
+ const int alpha = AImageDecoderHeaderInfo_getAlphaFlags(info);
+
+ if (alpha == ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE) {
+ // Set alpha, then scale. This succeeds for an opaque image.
+ int result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+ if (width * 2/3 == 0 || height * 2/3 == 0) {
+ // The image that is 1x1 cannot be downscaled.
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+ } else {
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ }
+
+ // Reset to the original settings to test the other order.
+ result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width, height);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ // Specify scale and then unpremul.
+ if (width * 2/3 == 0 || height * 2/3 == 0) {
+ // The image that is 1x1 cannot be downscaled. Scale up instead.
+ result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+ } else {
+ result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+ }
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ } else {
+ // Use unpremul and then scale. Setting to unpremul is successful, but
+ // later calls to change the scale fail.
+ int result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_SCALE, result);
+
+ // Set back to premul to verify that the opposite order also fails.
+ result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_PREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width * 2, height * 2);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+
+ result = AImageDecoder_setTargetSize(decoder, width * 2/3, height * 2/3);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_SUCCESS, result);
+ result = AImageDecoder_setAlphaFlags(decoder, ANDROID_BITMAP_FLAGS_ALPHA_UNPREMUL);
+ ASSERT_EQ(ANDROID_IMAGE_DECODER_INVALID_CONVERSION, result);
+ }
+}
+
+#define ASSET_MANAGER "Landroid/content/res/AssetManager;"
+#define STRING "Ljava/lang/String;"
+#define BITMAP "Landroid/graphics/Bitmap;"
+
+static JNINativeMethod gMethods[] = {
+ { "nTestEmptyCreate", "()V", (void*) testEmptyCreate },
+ { "nTestNullDecoder", "(" ASSET_MANAGER STRING ")V", (void*) testNullDecoder },
+ { "nTestInfo", "(JII" STRING "ZZ)V", (void*) testInfo },
+ { "nOpenAsset", "(" ASSET_MANAGER STRING ")J", (void*) openAssetNative },
+ { "nCloseAsset", "(J)V", (void*) closeAsset },
+ { "nCreateFromAsset", "(J)J", (void*) createFromAsset },
+ { "nCreateFromAssetFd", "(J)J", (void*) createFromAssetFd },
+ { "nCreateFromAssetBuffer", "(J)J", (void*) createFromAssetBuffer },
+ { "nCreateFromFd", "(I)J", (void*) createFromFd },
+ { "nTestCreateIncomplete", "(" ASSET_MANAGER STRING "I)V", (void*) testCreateIncomplete },
+ { "nTestCreateUnsupported", "(" ASSET_MANAGER STRING ")V", (void*) testCreateUnsupported },
+ { "nTestSetFormat", "(JZZ)V", (void*) testSetFormat },
+ { "nTestSetAlpha", "(JZ)V", (void*) testSetAlpha },
+ { "nTestGetMinimumStride", "(JZZ)V", (void*) testGetMinimumStride },
+ { "nTestDecode", "(JIZ" BITMAP ")V", (void*) testDecode },
+ { "nTestDecodeStride", "(J)V", (void*) testDecodeStride },
+ { "nTestSetTargetSize", "(J)V", (void*) testSetTargetSize },
+ { "nTestDecodeScaled", "(J" BITMAP ")V", (void*) testDecodeScaled },
+ { "nTestSetCrop", "(" ASSET_MANAGER STRING ")V", (void*) testSetCrop },
+ { "nTestDecodeCrop", "(J" BITMAP "IIIIII)V", (void*) testDecodeCrop },
+ { "nTestScalePlusUnpremul", "(J)V", (void*) testScalePlusUnpremul },
+};
+
+int register_android_graphics_cts_AImageDecoderTest(JNIEnv* env) {
+ jclass clazz = env->FindClass("android/graphics/cts/AImageDecoderTest");
+ return env->RegisterNatives(clazz, gMethods,
+ sizeof(gMethods) / sizeof(JNINativeMethod));
+}
+
diff --git a/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
new file mode 100644
index 0000000..d4daa18
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/AImageDecoderTest.java
@@ -0,0 +1,888 @@
+/*
+ * Copyright (C) 2019 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 android.system.OsConstants.SEEK_SET;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import android.content.ContentResolver;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.ImageDecoder;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+@RunWith(JUnitParamsRunner.class)
+public class AImageDecoderTest {
+ static {
+ System.loadLibrary("ctsgraphics_jni");
+ }
+
+ private static AssetManager getAssetManager() {
+ return InstrumentationRegistry.getTargetContext().getAssets();
+ }
+
+ private static Resources getResources() {
+ return InstrumentationRegistry.getTargetContext().getResources();
+ }
+
+ private static ContentResolver getContentResolver() {
+ return InstrumentationRegistry.getTargetContext().getContentResolver();
+ }
+
+ // These match the formats in the NDK.
+ // ANDROID_BITMAP_FORMAT_NONE is used by nTestDecode to signal using the default.
+ private static final int ANDROID_BITMAP_FORMAT_NONE = 0;
+ private static final int ANDROID_BITMAP_FORMAT_RGB_565 = 4;
+ private static final int ANDROID_BITMAP_FORMAT_A_8 = 8;
+ private static final int ANDROID_BITMAP_FORMAT_RGBA_F16 = 9;
+
+ @Test
+ public void testEmptyCreate() {
+ nTestEmptyCreate();
+ }
+
+ private static Object[] getAssetRecords() {
+ return ImageDecoderTest.getAssetRecords();
+ }
+
+ private static Object[] getRecords() {
+ return ImageDecoderTest.getRecords();
+ }
+
+ // For testing all of the assets as premul and unpremul.
+ private static Object[] getAssetRecordsUnpremul() {
+ return ImageDecoderTest.crossProduct(getAssetRecords(), new Object[] { true, false });
+ }
+
+ private static Object[] getRecordsUnpremul() {
+ return ImageDecoderTest.crossProduct(getRecords(), new Object[] { true, false });
+ }
+
+ // For testing all of the assets at different sample sizes.
+ private static Object[] getAssetRecordsSample() {
+ return ImageDecoderTest.crossProduct(getAssetRecords(), new Object[] { 2, 3, 4, 8, 16 });
+ }
+
+ private static Object[] getRecordsSample() {
+ return ImageDecoderTest.crossProduct(getRecords(), new Object[] { 2, 3, 4, 8, 16 });
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testNullDecoder(ImageDecoderTest.AssetRecord record) {
+ nTestNullDecoder(getAssetManager(), record.name);
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testCreateBuffer(ImageDecoderTest.AssetRecord record) {
+ // Note: This uses an asset for simplicity, but in native it gets a
+ // buffer.
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAssetBuffer(asset);
+
+ nTestInfo(aimagedecoder, record.width, record.height, "image/png",
+ false /* isAnimated */, record.isF16);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testCreateFd(ImageDecoderTest.AssetRecord record) {
+ // Note: This uses an asset for simplicity, but in native it gets a
+ // file descriptor.
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAssetFd(asset);
+
+ nTestInfo(aimagedecoder, record.width, record.height, "image/png",
+ false /* isAnimated */, record.isF16);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testCreateAsset(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestInfo(aimagedecoder, record.width, record.height, "image/png",
+ false /* isAnimated */, record.isF16);
+ nCloseAsset(asset);
+ }
+
+ private static ParcelFileDescriptor open(int resId, int offset) throws FileNotFoundException {
+ File file = Utils.obtainFile(resId, offset);
+ assertNotNull(file);
+
+ ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
+ ParcelFileDescriptor.MODE_READ_ONLY);
+ assertNotNull(pfd);
+ return pfd;
+ }
+
+ private static ParcelFileDescriptor open(int resId) throws FileNotFoundException {
+ return open(resId, 0);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testCreateFdResources(ImageDecoderTest.Record record) throws IOException {
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestInfo(aimagedecoder, record.width, record.height, record.mimeType,
+ false /* isAnimated */, false /*isF16*/);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testCreateFdOffset(ImageDecoderTest.Record record) throws IOException {
+ // Use an arbitrary offset. This ensures that we rewind to the correct offset.
+ final int offset = 15;
+ try (ParcelFileDescriptor pfd = open(record.resId, offset)) {
+ FileDescriptor fd = pfd.getFileDescriptor();
+ Os.lseek(fd, offset, SEEK_SET);
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestInfo(aimagedecoder, record.width, record.height, record.mimeType,
+ false /* isAnimated */, false /*isF16*/);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ } catch (ErrnoException err) {
+ fail("Failed to seek " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ public void testCreateIncomplete() {
+ String file = "green-srgb.png";
+ // This truncates the file before the IDAT.
+ nTestCreateIncomplete(getAssetManager(), file, 823);
+ }
+
+ @Test
+ @Parameters({"shaders/tri.frag", "test_video.mp4"})
+ public void testUnsupportedFormat(String file) {
+ nTestCreateUnsupported(getAssetManager(), file);
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testSetFormat(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestSetFormat(aimagedecoder, record.isF16, record.isGray);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testSetFormatResources(ImageDecoderTest.Record record) throws IOException {
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestSetFormat(aimagedecoder, false /* isF16 */, record.isGray);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testSetAlpha(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestSetAlpha(aimagedecoder, record.hasAlpha);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testSetAlphaResources(ImageDecoderTest.Record record) throws IOException {
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestSetAlpha(aimagedecoder, record.hasAlpha);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testGetMinimumStride(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestGetMinimumStride(aimagedecoder, record.isF16, record.isGray);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testGetMinimumStrideResources(ImageDecoderTest.Record record) throws IOException {
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestGetMinimumStride(aimagedecoder, false /* isF16 */, record.isGray);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ private static Bitmap decode(ImageDecoder.Source src, boolean unpremul) {
+ try {
+ return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+ // So we can compare pixels to the native decode.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+ decoder.setUnpremultipliedRequired(unpremul);
+ });
+ } catch (IOException e) {
+ fail("Failed to decode in Java with " + e);
+ return null;
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecordsUnpremul")
+ public void testDecode(ImageDecoderTest.AssetRecord record, boolean unpremul) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Bitmap bm = decode(src, unpremul);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_NONE, unpremul, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecordsUnpremul")
+ public void testDecodeResources(ImageDecoderTest.Record record, boolean unpremul)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ Bitmap bm = decode(src, unpremul);
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_NONE, unpremul, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ private static Bitmap decode(ImageDecoder.Source src, Bitmap.Config config) {
+ try {
+ return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+ // So we can compare pixels to the native decode.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+ switch (config) {
+ case RGB_565:
+ decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
+ break;
+ case ALPHA_8:
+ decoder.setDecodeAsAlphaMaskEnabled(true);
+ break;
+ default:
+ fail("Unexpected Config " + config);
+ break;
+ }
+ });
+ } catch (IOException e) {
+ fail("Failed to decode in Java with " + e);
+ return null;
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testDecode565(ImageDecoderTest.AssetRecord record) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Bitmap bm = decode(src, Bitmap.Config.RGB_565);
+
+ if (bm.getConfig() != Bitmap.Config.RGB_565) {
+ bm = null;
+ }
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGB_565, false, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testDecode565Resources(ImageDecoderTest.Record record)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ Bitmap bm = decode(src, Bitmap.Config.RGB_565);
+
+ if (bm.getConfig() != Bitmap.Config.RGB_565) {
+ bm = null;
+ }
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGB_565, false, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ @Parameters("grayscale-linearSrgb.png")
+ public void testDecodeA8(String name) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, name);
+ Bitmap bm = decode(src, Bitmap.Config.ALPHA_8);
+
+ assertNotNull(bm);
+ assertNull(bm.getColorSpace());
+ assertEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
+
+ long asset = nOpenAsset(assets, name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_A_8, false, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ public void testDecodeA8Resources()
+ throws IOException {
+ final int resId = R.drawable.grayscale_jpg;
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ resId);
+ Bitmap bm = decode(src, Bitmap.Config.ALPHA_8);
+
+ assertNotNull(bm);
+ assertNull(bm.getColorSpace());
+ assertEquals(Bitmap.Config.ALPHA_8, bm.getConfig());
+
+ try (ParcelFileDescriptor pfd = open(resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_A_8, false, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(resId));
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecordsUnpremul")
+ public void testDecodeF16(ImageDecoderTest.AssetRecord record, boolean unpremul) {
+ AssetManager assets = getAssetManager();
+
+ // ImageDecoder doesn't allow forcing a decode to F16, so use BitmapFactory.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.RGBA_F16;
+ options.inPremultiplied = !unpremul;
+
+ InputStream is = null;
+ try {
+ is = assets.open(record.name);
+ } catch (IOException e) {
+ fail("Failed to open " + record.name + " with " + e);
+ }
+ assertNotNull(is);
+
+ Bitmap bm = BitmapFactory.decodeStream(is, null, options);
+ assertNotNull(bm);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGBA_F16, unpremul, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecordsUnpremul")
+ public void testDecodeF16Resources(ImageDecoderTest.Record record, boolean unpremul)
+ throws IOException {
+ // ImageDecoder doesn't allow forcing a decode to F16, so use BitmapFactory.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.RGBA_F16;
+ options.inPremultiplied = !unpremul;
+ options.inScaled = false;
+
+ Bitmap bm = BitmapFactory.decodeResource(getResources(),
+ record.resId, options);
+ assertNotNull(bm);
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_RGBA_F16, unpremul, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testDecodeStride(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+ nTestDecodeStride(aimagedecoder);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testDecodeStrideResources(ImageDecoderTest.Record record)
+ throws IOException {
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecodeStride(aimagedecoder);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testSetTargetSize(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+ nTestSetTargetSize(aimagedecoder);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testSetTargetSizeResources(ImageDecoderTest.Record record)
+ throws IOException {
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestSetTargetSize(aimagedecoder);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + Utils.getAsResourceUri(record.resId));
+ }
+ }
+
+ private Bitmap decodeSampled(String name, ImageDecoder.Source src, int sampleSize) {
+ try {
+ return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+ // So we can compare pixels to the native decode.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+ decoder.setTargetSampleSize(sampleSize);
+ });
+ } catch (IOException e) {
+ fail("Failed to decode " + name + " in Java (sampleSize: "
+ + sampleSize + ") with " + e);
+ return null;
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecordsSample")
+ public void testDecodeSampled(ImageDecoderTest.AssetRecord record, int sampleSize) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Bitmap bm = decodeSampled(record.name, src, sampleSize);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecodeScaled(aimagedecoder, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecordsSample")
+ public void testDecodeResourceSampled(ImageDecoderTest.Record record, int sampleSize)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ String name = Utils.getAsResourceUri(record.resId).toString();
+ Bitmap bm = decodeSampled(name, src, sampleSize);
+ assertNotNull(bm);
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecodeScaled(aimagedecoder, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + name + ": " + e);
+ }
+ }
+
+ private Bitmap decodeScaled(String name, ImageDecoder.Source src) {
+ try {
+ return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+ // So we can compare pixels to the native decode.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+
+ // Scale to an arbitrary width and height.
+ decoder.setTargetSize(300, 300);
+ });
+ } catch (IOException e) {
+ fail("Failed to decode " + name + " in Java (size: "
+ + "300 x 300) with " + e);
+ return null;
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testDecodeScaled(ImageDecoderTest.AssetRecord record) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Bitmap bm = decodeScaled(record.name, src);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecodeScaled(aimagedecoder, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testDecodeResourceScaled(ImageDecoderTest.Record record)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ String name = Utils.getAsResourceUri(record.resId).toString();
+ Bitmap bm = decodeScaled(name, src);
+ assertNotNull(bm);
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecodeScaled(aimagedecoder, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + name + ": " + e);
+ }
+ }
+
+ private Bitmap decodeScaleUp(String name, ImageDecoder.Source src) {
+ try {
+ return ImageDecoder.decodeBitmap(src, (decoder, info, source) -> {
+ // So we can compare pixels to the native decode.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+
+ decoder.setTargetSize(info.getSize().getWidth() * 2,
+ info.getSize().getHeight() * 2);
+ });
+ } catch (IOException e) {
+ fail("Failed to decode " + name + " in Java (scaled up) with " + e);
+ return null;
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testDecodeScaleUp(ImageDecoderTest.AssetRecord record) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Bitmap bm = decodeScaleUp(record.name, src);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestDecodeScaled(aimagedecoder, bm);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testDecodeResourceScaleUp(ImageDecoderTest.Record record)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ String name = Utils.getAsResourceUri(record.resId).toString();
+ Bitmap bm = decodeScaleUp(name, src);
+ assertNotNull(bm);
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecodeScaled(aimagedecoder, bm);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + name + ": " + e);
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testSetCrop(ImageDecoderTest.AssetRecord record) {
+ nTestSetCrop(getAssetManager(), record.name);
+ }
+
+ private static class Cropper implements ImageDecoder.OnHeaderDecodedListener {
+ Cropper(boolean scale) {
+ mScale = scale;
+ }
+
+ public boolean withScale() {
+ return mScale;
+ }
+
+ public int getWidth() {
+ return mWidth;
+ }
+
+ public int getHeight() {
+ return mHeight;
+ }
+
+ public Rect getCropRect() {
+ return mCropRect;
+ }
+
+ @Override
+ public void onHeaderDecoded(ImageDecoder decoder, ImageDecoder.ImageInfo info,
+ ImageDecoder.Source source) {
+ mWidth = info.getSize().getWidth();
+ mHeight = info.getSize().getHeight();
+ if (mScale) {
+ mWidth = 40;
+ mHeight = 40;
+ decoder.setTargetSize(mWidth, mHeight);
+ }
+
+ mCropRect = new Rect(mWidth / 2, mHeight / 2, mWidth, mHeight);
+ decoder.setCrop(mCropRect);
+
+ // So we can compare pixels to the native decode.
+ decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
+ }
+
+ private final boolean mScale;
+ private Rect mCropRect;
+ private int mWidth;
+ private int mHeight;
+ }
+
+ private static Bitmap decodeCropped(String name, Cropper cropper, ImageDecoder.Source src) {
+ try {
+ return ImageDecoder.decodeBitmap(src, cropper);
+ } catch (IOException e) {
+ fail("Failed to decode " + name + " in Java with "
+ + (cropper.withScale() ? "scale and " : "a ") + "crop ("
+ + cropper.getCropRect() + "): " + e);
+ return null;
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testCrop(ImageDecoderTest.AssetRecord record) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Cropper cropper = new Cropper(false /* scale */);
+ Bitmap bm = decodeCropped(record.name, cropper, src);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ Rect crop = cropper.getCropRect();
+ nTestDecodeCrop(aimagedecoder, bm, 0, 0, crop.left, crop.top, crop.right, crop.bottom);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testCropResource(ImageDecoderTest.Record record)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ String name = Utils.getAsResourceUri(record.resId).toString();
+ Cropper cropper = new Cropper(false /* scale */);
+ Bitmap bm = decodeCropped(name, cropper, src);
+ assertNotNull(bm);
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ Rect crop = cropper.getCropRect();
+ nTestDecodeCrop(aimagedecoder, bm, 0, 0, crop.left, crop.top, crop.right, crop.bottom);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + name + ": " + e);
+ }
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testCropAndScale(ImageDecoderTest.AssetRecord record) {
+ AssetManager assets = getAssetManager();
+ ImageDecoder.Source src = ImageDecoder.createSource(assets, record.name);
+ Cropper cropper = new Cropper(true /* scale */);
+ Bitmap bm = decodeCropped(record.name, cropper, src);
+
+ long asset = nOpenAsset(assets, record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ Rect crop = cropper.getCropRect();
+ nTestDecodeCrop(aimagedecoder, bm, cropper.getWidth(), cropper.getHeight(),
+ crop.left, crop.top, crop.right, crop.bottom);
+ nCloseAsset(asset);
+ }
+
+ @Test
+ @Parameters(method = "getRecords")
+ public void testCropAndScaleResource(ImageDecoderTest.Record record)
+ throws IOException {
+ ImageDecoder.Source src = ImageDecoder.createSource(getResources(),
+ record.resId);
+ String name = Utils.getAsResourceUri(record.resId).toString();
+ Cropper cropper = new Cropper(true /* scale */);
+ Bitmap bm = decodeCropped(name, cropper, src);
+ assertNotNull(bm);
+
+ try (ParcelFileDescriptor pfd = open(record.resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ Rect crop = cropper.getCropRect();
+ nTestDecodeCrop(aimagedecoder, bm, cropper.getWidth(), cropper.getHeight(),
+ crop.left, crop.top, crop.right, crop.bottom);
+ } catch (FileNotFoundException e) {
+ fail("Could not open " + name + ": " + e);
+ }
+ }
+
+ private static Object[] getExifImages() {
+ return new Object[] {
+ 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,
+ };
+ }
+
+ @Test
+ @Parameters(method = "getExifImages")
+ public void testRespectOrientation(int resId) throws IOException {
+ Uri uri = Utils.getAsResourceUri(resId);
+ ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(),
+ uri);
+ Bitmap bm = decode(src, false /* unpremul */);
+ assertNotNull(bm);
+ assertEquals(100, bm.getWidth());
+ assertEquals(80, bm.getHeight());
+
+ try (ParcelFileDescriptor pfd = open(resId)) {
+ long aimagedecoder = nCreateFromFd(pfd.getFd());
+
+ nTestDecode(aimagedecoder, ANDROID_BITMAP_FORMAT_NONE, false /* unpremul */, bm);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ fail("Could not open " + uri);
+ }
+ bm.recycle();
+ }
+
+ @Test
+ @Parameters(method = "getAssetRecords")
+ public void testScalePlusUnpremul(ImageDecoderTest.AssetRecord record) {
+ long asset = nOpenAsset(getAssetManager(), record.name);
+ long aimagedecoder = nCreateFromAsset(asset);
+
+ nTestScalePlusUnpremul(aimagedecoder);
+ nCloseAsset(asset);
+ }
+
+ // Return a pointer to the native AAsset named |file|. Must be closed with nCloseAsset.
+ // Throws an Exception on failure.
+ private static native long nOpenAsset(AssetManager assets, String file);
+ private static native void nCloseAsset(long asset);
+
+ // Methods for creating and returning a pointer to an AImageDecoder. All
+ // throw an Exception on failure.
+ private static native long nCreateFromFd(int fd);
+ private static native long nCreateFromAsset(long asset);
+ private static native long nCreateFromAssetFd(long asset);
+ private static native long nCreateFromAssetBuffer(long asset);
+
+ private static native void nTestEmptyCreate();
+ private static native void nTestNullDecoder(AssetManager assets, String file);
+ private static native void nTestCreateIncomplete(AssetManager assets,
+ String file, int truncatedLength);
+ private static native void nTestCreateUnsupported(AssetManager assets, String file);
+
+ // For convenience, all methods that take aimagedecoder as a parameter delete
+ // it.
+ private static native void nTestInfo(long aimagedecoder, int width, int height,
+ String mimeType, boolean isAnimated, boolean isF16);
+ private static native void nTestSetFormat(long aimagedecoder, boolean isF16, boolean isGray);
+ private static native void nTestSetAlpha(long aimagedecoder, boolean hasAlpha);
+ private static native void nTestGetMinimumStride(long aimagedecoder,
+ boolean isF16, boolean isGray);
+ private static native void nTestDecode(long aimagedecoder,
+ int requestedAndroidBitmapFormat, boolean unpremul, Bitmap bitmap);
+ private static native void nTestDecodeStride(long aimagedecoder);
+ private static native void nTestSetTargetSize(long aimagedecoder);
+ // Decode with the target width and height to match |bitmap|.
+ private static native void nTestDecodeScaled(long aimagedecoder, Bitmap bitmap);
+ private static native void nTestSetCrop(AssetManager assets, String file);
+ // Decode and compare to |bitmap|, where they both use the specified target
+ // size and crop rect. target size of 0 means to skip scaling.
+ private static native void nTestDecodeCrop(long aimagedecoder,
+ Bitmap bitmap, int targetWidth, int targetHeight,
+ int cropLeft, int cropTop, int cropRight, int cropBottom);
+ private static native void nTestScalePlusUnpremul(long aimagedecoder);
+}
diff --git a/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java b/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java
index 959aeef..6fcc5c0 100644
--- a/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/BitmapFactoryTest.java
@@ -354,7 +354,7 @@
opts.inInputShareable = purgeable;
long actualOffset = actual_offsets[j];
- String path = obtainPath(testImage.id, actualOffset);
+ String path = Utils.obtainPath(testImage.id, actualOffset);
RandomAccessFile file = new RandomAccessFile(path, "r");
FileDescriptor fd = file.getFD();
assertTrue(fd.valid());
@@ -1015,38 +1015,6 @@
}
private String obtainPath() throws IOException {
- return obtainPath(R.drawable.start, 0);
- }
-
- /*
- * Create a new file and return a path to it.
- * @param resId Original file. It will be copied into the new file.
- * @param offset Number of zeroes to write to the new file before the
- * copied file. This allows testing decodeFileDescriptor
- * with an offset. Must be less than or equal to 1024
- */
- private String obtainPath(int resId, long offset) throws IOException {
- File dir = InstrumentationRegistry.getTargetContext().getFilesDir();
- dir.mkdirs();
- // The suffix does not necessarily represent theactual file type.
- File file = new File(dir, "test.jpg");
- if (!file.createNewFile()) {
- if (!file.exists()) {
- fail("Failed to create new File!");
- }
- }
- InputStream is = obtainInputStream(resId);
- FileOutputStream fOutput = new FileOutputStream(file);
- byte[] dataBuffer = new byte[1024];
- // Write a bunch of zeroes before the image.
- assertTrue(offset <= 1024);
- fOutput.write(dataBuffer, 0, (int) offset);
- int readLength = 0;
- while ((readLength = is.read(dataBuffer)) != -1) {
- fOutput.write(dataBuffer, 0, readLength);
- }
- is.close();
- fOutput.close();
- return (file.getPath());
+ return Utils.obtainPath(R.drawable.start, 0);
}
}
diff --git a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
index fe13ea4..f6c278a 100644
--- a/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
+++ b/tests/tests/graphics/src/android/graphics/cts/ImageDecoderTest.java
@@ -75,35 +75,41 @@
@RunWith(JUnitParamsRunner.class)
public class ImageDecoderTest {
- private static final class Record {
+ static final class Record {
public final int resId;
public final int width;
public final int height;
+ public final boolean isGray;
+ public final boolean hasAlpha;
public final String mimeType;
public final ColorSpace colorSpace;
- Record(int resId, int width, int height, String mimeType, ColorSpace colorSpace) {
+ Record(int resId, int width, int height, String mimeType, boolean isGray,
+ boolean hasAlpha, ColorSpace colorSpace) {
this.resId = resId;
this.width = width;
this.height = height;
this.mimeType = mimeType;
+ this.isGray = isGray;
+ this.hasAlpha = hasAlpha;
this.colorSpace = colorSpace;
}
}
private static final ColorSpace sSRGB = ColorSpace.get(ColorSpace.Named.SRGB);
- private Object[] getRecords() {
+ static Object[] getRecords() {
return 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),
- new Record(R.raw.heifwriter_input, 1920, 1080, "image/heif", sSRGB),
+ new Record(R.drawable.baseline_jpeg, 1280, 960, "image/jpeg", false, false, sSRGB),
+ new Record(R.drawable.grayscale_jpg, 128, 128, "image/jpeg", true, false, sSRGB),
+ new Record(R.drawable.png_test, 640, 480, "image/png", false, false, sSRGB),
+ new Record(R.drawable.gif_test, 320, 240, "image/gif", false, false, sSRGB),
+ new Record(R.drawable.bmp_test, 320, 240, "image/bmp", false, false, sSRGB),
+ new Record(R.drawable.webp_test, 640, 480, "image/webp", false, false, sSRGB),
+ new Record(R.drawable.google_chrome, 256, 256, "image/x-ico", false, true, sSRGB),
+ new Record(R.drawable.color_wheel, 128, 128, "image/x-ico", false, true, sSRGB),
+ new Record(R.raw.sample_1mp, 600, 338, "image/x-adobe-dng", false, false, sSRGB),
+ new Record(R.raw.heifwriter_input, 1920, 1080, "image/heif", false, false, sSRGB),
};
}
@@ -115,7 +121,7 @@
return output.toByteArray();
}
- private static void writeToStream(OutputStream output, int resId, int offset, int extra) {
+ static void writeToStream(OutputStream output, int resId, int offset, int extra) {
InputStream input = getResources().openRawResource(resId);
byte[] buffer = new byte[4096];
int bytesRead;
@@ -194,16 +200,6 @@
"android.graphics.cts.fileprovider", getAsFile(resId));
}
- private Uri getAsResourceUri(int resId) {
- Resources res = getResources();
- return new Uri.Builder()
- .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
- .authority(res.getResourcePackageName(resId))
- .appendPath(res.getResourceTypeName(resId))
- .appendPath(res.getResourceEntryName(resId))
- .build();
- }
-
private Callable<AssetFileDescriptor> getAsCallable(int resId) {
final Context context = InstrumentationRegistry.getTargetContext();
final Uri uri = getAsContentUri(resId);
@@ -225,7 +221,7 @@
private interface UriCreator extends IntFunction<Uri> {};
private UriCreator[] mUriCreators = new UriCreator[] {
- resId -> getAsResourceUri(resId),
+ resId -> Utils.getAsResourceUri(resId),
resId -> getAsFileUri(resId),
resId -> getAsContentUri(resId),
};
@@ -260,7 +256,7 @@
return InstrumentationRegistry.getTargetContext().getResources();
}
- private ContentResolver getContentResolver() {
+ private static ContentResolver getContentResolver() {
return InstrumentationRegistry.getTargetContext().getContentResolver();
}
@@ -278,7 +274,7 @@
assertSame(record.colorSpace, info.getColorSpace());
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
}
}
}
@@ -357,7 +353,7 @@
}
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -398,7 +394,7 @@
try {
bm = ImageDecoder.decodeBitmap(src, l);
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(record.resId)
+ fail("Failed " + Utils.getAsResourceUri(record.resId)
+ " with exception " + e);
}
assertNotNull(bm);
@@ -421,7 +417,7 @@
}
break;
default:
- String name = getAsResourceUri(record.resId).toString();
+ String name = Utils.getAsResourceUri(record.resId).toString();
assertEquals("image " + name + "; allocator: " + allocator,
Bitmap.Config.HARDWARE, bm.getConfig());
break;
@@ -446,7 +442,7 @@
assertFalse(decoder.isUnpremultipliedRequired());
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -501,7 +497,7 @@
}
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -728,7 +724,7 @@
assertEquals(pp.width, drawable.getIntrinsicWidth());
assertEquals(pp.height, drawable.getIntrinsicHeight());
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
}
}
}
@@ -751,7 +747,7 @@
@Test
@Parameters(method = "getRecords")
public void testSampleSize(Record record) {
- final String name = getAsResourceUri(record.resId).toString();
+ final String name = Utils.getAsResourceUri(record.resId).toString();
for (int sampleSize : new int[] { 2, 3, 4, 8, 32 }) {
ImageDecoder.Source src = mCreators[0].apply(record.resId);
try {
@@ -785,7 +781,7 @@
});
assertEquals(1, dr.getIntrinsicWidth());
} catch (Exception e) {
- String file = getAsResourceUri(record.resId).toString();
+ String file = Utils.getAsResourceUri(record.resId).toString();
fail("Failed to decode " + file + " with exception " + e);
}
}
@@ -861,7 +857,7 @@
}
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -970,6 +966,11 @@
// FIXME (scroggo): Some codecs currently do not support incomplete images.
return;
}
+ if (record.resId == R.drawable.grayscale_jpg) {
+ // FIXME (scroggo): This is a progressive jpeg. If Skia switches to
+ // decoding jpegs progressively, this image can be partially decoded.
+ return;
+ }
for (boolean abort : abortDecode) {
ImageDecoder.Source src = ImageDecoder.createSource(
ByteBuffer.wrap(bytes, 0, truncatedLength));
@@ -1072,7 +1073,7 @@
assertFalse(decoder.isMutableRequired());
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -1109,7 +1110,7 @@
assertTrue(bm.isMutable());
assertNotEquals(Bitmap.Config.HARDWARE, bm.getConfig());
} catch (Exception e) {
- String file = getAsResourceUri(record.resId).toString();
+ String file = Utils.getAsResourceUri(record.resId).toString();
fail("Failed to decode " + file + " with exception " + e);
}
}
@@ -1229,7 +1230,7 @@
assertEquals(l.width, bm.getWidth());
assertEquals(l.height, bm.getHeight());
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(record.resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(record.resId) + " with exception " + e);
}
}
@@ -1336,7 +1337,7 @@
assertEquals(r, decoder.getCrop());
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -1383,7 +1384,7 @@
assertEquals(l.cropRect.width(), drawable.getIntrinsicWidth());
assertEquals(l.cropRect.height(), drawable.getIntrinsicHeight());
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(record.resId)
+ fail("Failed " + Utils.getAsResourceUri(record.resId)
+ " with exception " + e);
}
}
@@ -1740,7 +1741,7 @@
assertFalse(decoder.isDecodeAsAlphaMaskEnabled());
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -1822,7 +1823,7 @@
assertEquals(ImageDecoder.MEMORY_POLICY_DEFAULT, decoder.getMemorySizePolicy());
});
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with exception " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with exception " + e);
}
}
@@ -1972,7 +1973,7 @@
reference = null;
isWebp = true;
}
- Uri uri = getAsResourceUri(resId);
+ Uri uri = Utils.getAsResourceUri(resId);
ImageDecoder.Source src = ImageDecoder.createSource(getContentResolver(), uri);
try {
Bitmap bm = ImageDecoder.decodeBitmap(src, (decoder, info, s) -> {
@@ -2099,7 +2100,7 @@
Drawable drawable = ImageDecoder.decodeDrawable(src);
assertNotNull(drawable);
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(record.resId) + " with " + e);
+ fail("Failed " + Utils.getAsResourceUri(record.resId) + " with " + e);
}
}
@@ -2111,7 +2112,7 @@
assertTrue(drawable instanceof BitmapDrawable);
return (BitmapDrawable) drawable;
} catch (IOException e) {
- fail("Failed " + getAsResourceUri(resId) + " with " + e);
+ fail("Failed " + Utils.getAsResourceUri(resId) + " with " + e);
return null;
}
}
@@ -2168,21 +2169,23 @@
}
}
- private static class AssetRecord {
+ static class AssetRecord {
public final String name;
public final int width;
public final int height;
public final boolean isF16;
public final boolean isGray;
+ public final boolean hasAlpha;
private final ColorSpace mColorSpace;
- AssetRecord(String name, int width, int height, boolean isF16, boolean isGray,
- ColorSpace colorSpace) {
+ AssetRecord(String name, int width, int height, boolean isF16,
+ boolean isGray, boolean hasAlpha, ColorSpace colorSpace) {
this.name = name;
this.width = width;
this.height = height;
this.isF16 = isF16;
this.isGray = isGray;
+ this.hasAlpha = hasAlpha;
mColorSpace = colorSpace;
}
@@ -2207,24 +2210,24 @@
}
}
- private Object [] getAssetRecords() {
+ static Object[] getAssetRecords() {
return new Object [] {
// A null ColorSpace means that the color space is "Unknown".
- new AssetRecord("almost-red-adobe.png", 1, 1, false, false, null),
- new AssetRecord("green-p3.png", 64, 64, false, false,
+ new AssetRecord("almost-red-adobe.png", 1, 1, false, false, false, null),
+ new AssetRecord("green-p3.png", 64, 64, false, false, false,
ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
- new AssetRecord("green-srgb.png", 64, 64, false, false, sSRGB),
- new AssetRecord("blue-16bit-prophoto.png", 100, 100, true, false,
+ new AssetRecord("green-srgb.png", 64, 64, false, false, false, sSRGB),
+ new AssetRecord("blue-16bit-prophoto.png", 100, 100, true, false, true,
ColorSpace.get(ColorSpace.Named.PRO_PHOTO_RGB)),
- new AssetRecord("blue-16bit-srgb.png", 64, 64, true, false,
+ new AssetRecord("blue-16bit-srgb.png", 64, 64, true, false, false,
ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)),
- new AssetRecord("purple-cmyk.png", 64, 64, false, false, sSRGB),
- new AssetRecord("purple-displayprofile.png", 64, 64, false, false, null),
- new AssetRecord("red-adobergb.png", 64, 64, false, false,
+ new AssetRecord("purple-cmyk.png", 64, 64, false, false, false, sSRGB),
+ new AssetRecord("purple-displayprofile.png", 64, 64, false, false, false, null),
+ new AssetRecord("red-adobergb.png", 64, 64, false, false, false,
ColorSpace.get(ColorSpace.Named.ADOBE_RGB)),
- new AssetRecord("translucent-green-p3.png", 64, 64, false, false,
+ new AssetRecord("translucent-green-p3.png", 64, 64, false, false, true,
ColorSpace.get(ColorSpace.Named.DISPLAY_P3)),
- new AssetRecord("grayscale-linearSrgb.png", 32, 32, false, true,
+ new AssetRecord("grayscale-linearSrgb.png", 32, 32, false, true, false,
ColorSpace.get(ColorSpace.Named.LINEAR_SRGB)),
};
}
@@ -2247,6 +2250,7 @@
assertEquals(record.name, record.width, bm.getWidth());
assertEquals(record.name, record.height, bm.getHeight());
record.checkColorSpace(null, bm.getColorSpace());
+ assertEquals(record.hasAlpha, bm.hasAlpha());
} catch (IOException e) {
fail("Failed to decode asset " + record.name + " with " + e);
}
@@ -2471,7 +2475,7 @@
}
- private Object[] crossProduct(Object[] a, Object[] b) {
+ static Object[] crossProduct(Object[] a, Object[] b) {
final int length = a.length * b.length;
Object[] ret = new Object[length];
for (int i = 0; i < a.length; i++) {
@@ -2498,7 +2502,7 @@
return;
}
- String name = getAsResourceUri(record.resId).toString();
+ String name = Utils.getAsResourceUri(record.resId).toString();
ImageDecoder.Source src = f.apply(record.resId);
testReuse(src, name);
}
@@ -2511,7 +2515,7 @@
return;
}
- String name = getAsResourceUri(record.resId).toString();
+ String name = Utils.getAsResourceUri(record.resId).toString();
ImageDecoder.Source src = ImageDecoder.createSource(getResources(), record.resId);
testReuse(src, name);
diff --git a/tests/tests/graphics/src/android/graphics/cts/Utils.java b/tests/tests/graphics/src/android/graphics/cts/Utils.java
new file mode 100644
index 0000000..f7229ed
--- /dev/null
+++ b/tests/tests/graphics/src/android/graphics/cts/Utils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.net.Uri;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class Utils {
+ private static Resources getResources() {
+ return InstrumentationRegistry.getTargetContext().getResources();
+ }
+
+ private static InputStream obtainInputStream(int resId) {
+ return getResources().openRawResource(resId);
+ }
+
+ static Uri getAsResourceUri(int resId) {
+ Resources res = getResources();
+ return new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority(res.getResourcePackageName(resId))
+ .appendPath(res.getResourceTypeName(resId))
+ .appendPath(res.getResourceEntryName(resId))
+ .build();
+ }
+
+ /*
+ * Create a new file and return a path to it.
+ * @param resId Original file. It will be copied into the new file.
+ * @param offset Number of zeroes to write to the new file before the
+ * copied file. This allows testing decodeFileDescriptor
+ * with an offset. Must be less than or equal to 1024
+ */
+ static String obtainPath(int resId, long offset) {
+ return obtainFile(resId, offset).getPath();
+ }
+
+ /*
+ * Create and return a new file.
+ * @param resId Original file. It will be copied into the new file.
+ * @param offset Number of zeroes to write to the new file before the
+ * copied file. This allows testing decodeFileDescriptor
+ * with an offset. Must be less than or equal to 1024
+ */
+ static File obtainFile(int resId, long offset) {
+ assertTrue(offset >= 0);
+ File dir = InstrumentationRegistry.getTargetContext().getFilesDir();
+ dir.mkdirs();
+
+ String name = getResources().getResourceEntryName(resId).toString();
+ if (offset > 0) {
+ name = name + "_" + String.valueOf(offset);
+ }
+
+ File file = new File(dir, name);
+ if (file.exists()) {
+ return file;
+ }
+
+ try {
+ file.createNewFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ // If the file does not exist it will be handled below.
+ }
+ if (!file.exists()) {
+ fail("Failed to create new File for " + name + "!");
+ }
+
+ InputStream is = obtainInputStream(resId);
+
+ try {
+ FileOutputStream fOutput = new FileOutputStream(file);
+ byte[] dataBuffer = new byte[1024];
+ // Write a bunch of zeroes before the image.
+ assertTrue(offset <= 1024);
+ fOutput.write(dataBuffer, 0, (int) offset);
+ int readLength = 0;
+ while ((readLength = is.read(dataBuffer)) != -1) {
+ fOutput.write(dataBuffer, 0, readLength);
+ }
+ is.close();
+ fOutput.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ fail("Failed to create file \"" + name + "\" with exception " + e);
+ }
+ return file;
+ }
+}