From 0c01dbf8f283b7bcaf6babf540a501735c73b4f5 Mon Sep 17 00:00:00 2001 From: Leon Scroggins III Date: Fri, 20 Oct 2017 14:08:11 -0400 Subject: [PATCH] ImageDecoder (BitmapFactory 2.0) Bug: 63909536 Bug: 63908092 Test: CTS: I0f36ce34c968fd7fae4d8edebabea3a421859615 One-pager: https://docs.google.com/document/d/1IWSdXb5O9lu-Zbj7SaNWo5pS7-FHlonFnqazjnecozM/ Design doc: https://docs.google.com/document/d/15S6DSAV4EwOuJLv29UC_9cdSGdPg3KvOJVn2EHoP3fw/ ImageDecoder is designed to streamline certain patterns of BitmapFactory use: - choosing sample size based on actual dimensions - choosing a specific output size - post-processing (e.g. for rounded corners) - copying to HARDWARE - decode directly to ashmem - creating a Drawable - use as an alpha mask - save RAM (e.g. use RGB_565) In addition, it will include new features: - animated drawables (TODO) - report failures *and* optionally create a partial image - crop Add PostProcess to handle post-processing. It is separate from ImageDecoder so that it may be used in the future by other commands that might want something similar (e.g. capturing a View). Consolidate NinePatch code for sharing between BitmapFactory and ImageDecoder. Some features left out of this CL: - Create from ContentResolver + URI - animation - report more info in ImageInfo - more overloads (e.g. null OnHeaderDecodedListener) Change-Id: Icf011dc1b97b492788e47cf51fcf8abe8e9c7b88 --- core/jni/Android.bp | 2 + core/jni/AndroidRuntime.cpp | 4 + core/jni/android/graphics/BitmapFactory.cpp | 62 +- .../graphics/ByteBufferStreamAdaptor.cpp | 323 +++++++++ .../graphics/ByteBufferStreamAdaptor.h | 37 + core/jni/android/graphics/ImageDecoder.cpp | 473 +++++++++++++ core/jni/android/graphics/NinePatch.cpp | 32 + core/jni/android/graphics/NinePatchPeeker.cpp | 42 +- core/jni/android/graphics/NinePatchPeeker.h | 7 +- .../java/android/graphics/ImageDecoder.java | 665 ++++++++++++++++++ .../java/android/graphics/PostProcess.java | 91 +++ 11 files changed, 1677 insertions(+), 61 deletions(-) create mode 100644 core/jni/android/graphics/ByteBufferStreamAdaptor.cpp create mode 100644 core/jni/android/graphics/ByteBufferStreamAdaptor.h create mode 100644 core/jni/android/graphics/ImageDecoder.cpp create mode 100644 graphics/java/android/graphics/ImageDecoder.java create mode 100644 graphics/java/android/graphics/PostProcess.java diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 8df0fb891f1c9..297bae25c3f31 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -123,6 +123,7 @@ cc_library_shared { "android_graphics_Picture.cpp", "android/graphics/Bitmap.cpp", "android/graphics/BitmapFactory.cpp", + "android/graphics/ByteBufferStreamAdaptor.cpp", "android/graphics/Camera.cpp", "android/graphics/CanvasProperty.cpp", "android/graphics/ColorFilter.cpp", @@ -134,6 +135,7 @@ cc_library_shared { "android/graphics/GraphicBuffer.cpp", "android/graphics/Graphics.cpp", "android/graphics/HarfBuzzNGFaceSkia.cpp", + "android/graphics/ImageDecoder.cpp", "android/graphics/Interpolator.cpp", "android/graphics/MaskFilter.cpp", "android/graphics/Matrix.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 9e907bdf458fd..9ab73d4fc7aaf 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -57,10 +57,12 @@ extern int register_android_os_Process(JNIEnv* env); extern int register_android_graphics_Bitmap(JNIEnv*); extern int register_android_graphics_BitmapFactory(JNIEnv*); extern int register_android_graphics_BitmapRegionDecoder(JNIEnv*); +extern int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Camera(JNIEnv* env); extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); extern int register_android_graphics_GraphicBuffer(JNIEnv* env); extern int register_android_graphics_Graphics(JNIEnv* env); +extern int register_android_graphics_ImageDecoder(JNIEnv*); extern int register_android_graphics_Interpolator(JNIEnv* env); extern int register_android_graphics_MaskFilter(JNIEnv* env); extern int register_android_graphics_Movie(JNIEnv* env); @@ -1380,6 +1382,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_Bitmap), REG_JNI(register_android_graphics_BitmapFactory), REG_JNI(register_android_graphics_BitmapRegionDecoder), + REG_JNI(register_android_graphics_ByteBufferStreamAdaptor), REG_JNI(register_android_graphics_Camera), REG_JNI(register_android_graphics_CreateJavaOutputStreamAdaptor), REG_JNI(register_android_graphics_CanvasProperty), @@ -1387,6 +1390,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_graphics_DrawFilter), REG_JNI(register_android_graphics_FontFamily), REG_JNI(register_android_graphics_GraphicBuffer), + REG_JNI(register_android_graphics_ImageDecoder), REG_JNI(register_android_graphics_Interpolator), REG_JNI(register_android_graphics_MaskFilter), REG_JNI(register_android_graphics_Matrix), diff --git a/core/jni/android/graphics/BitmapFactory.cpp b/core/jni/android/graphics/BitmapFactory.cpp index 2e8c27ab7217e..79c5a340f16f7 100644 --- a/core/jni/android/graphics/BitmapFactory.cpp +++ b/core/jni/android/graphics/BitmapFactory.cpp @@ -47,9 +47,6 @@ jfieldID gOptions_bitmapFieldID; jfieldID gBitmap_ninePatchInsetsFieldID; -jclass gInsetStruct_class; -jmethodID gInsetStruct_constructorMethodID; - jclass gBitmapConfig_class; jmethodID gBitmapConfig_nativeToConfigMethodID; @@ -99,43 +96,6 @@ jstring encodedFormatToString(JNIEnv* env, SkEncodedImageFormat format) { return jstr; } -static void scaleDivRange(int32_t* divs, int count, float scale, int maxValue) { - for (int i = 0; i < count; i++) { - divs[i] = int32_t(divs[i] * scale + 0.5f); - if (i > 0 && divs[i] == divs[i - 1]) { - divs[i]++; // avoid collisions - } - } - - if (CC_UNLIKELY(divs[count - 1] > maxValue)) { - // if the collision avoidance above put some divs outside the bounds of the bitmap, - // slide outer stretchable divs inward to stay within bounds - int highestAvailable = maxValue; - for (int i = count - 1; i >= 0; i--) { - divs[i] = highestAvailable; - if (i > 0 && divs[i] <= divs[i-1]){ - // keep shifting - highestAvailable = divs[i] - 1; - } else { - break; - } - } - } -} - -static void scaleNinePatchChunk(android::Res_png_9patch* chunk, float scale, - int scaledWidth, int scaledHeight) { - chunk->paddingLeft = int(chunk->paddingLeft * scale + 0.5f); - chunk->paddingTop = int(chunk->paddingTop * scale + 0.5f); - chunk->paddingRight = int(chunk->paddingRight * scale + 0.5f); - chunk->paddingBottom = int(chunk->paddingBottom * scale + 0.5f); - - // The max value for the divRange is one pixel less than the actual max to ensure that the size - // of the last div is not zero. A div of size 0 is considered invalid input and will not render. - scaleDivRange(chunk->getXDivs(), chunk->numXDivs, scale, scaledWidth - 1); - scaleDivRange(chunk->getYDivs(), chunk->numYDivs, scale, scaledHeight - 1); -} - class ScaleCheckingAllocator : public SkBitmap::HeapAllocator { public: ScaleCheckingAllocator(float scale, int size) @@ -428,7 +388,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr stream, jbyteArray ninePatchChunk = NULL; if (peeker.mPatch != NULL) { if (willScale) { - scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight); + peeker.scale(scale, scale, scaledWidth, scaledHeight); } size_t ninePatchArraySize = peeker.mPatch->serializedSize(); @@ -448,12 +408,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr stream, jobject ninePatchInsets = NULL; if (peeker.mHasInsets) { - ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID, - peeker.mOpticalInsets[0], peeker.mOpticalInsets[1], - peeker.mOpticalInsets[2], peeker.mOpticalInsets[3], - peeker.mOutlineInsets[0], peeker.mOutlineInsets[1], - peeker.mOutlineInsets[2], peeker.mOutlineInsets[3], - peeker.mOutlineRadius, peeker.mOutlineAlpha, scale); + ninePatchInsets = peeker.createNinePatchInsets(env, scale); if (ninePatchInsets == NULL) { return nullObjectReturn("nine patch insets == null"); } @@ -508,13 +463,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr stream, } if (padding) { - if (peeker.mPatch != NULL) { - GraphicsJNI::set_jrect(env, padding, - peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop, - peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom); - } else { - GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1); - } + peeker.getPadding(env, padding); } // If we get here, the outputBitmap should have an installed pixelref. @@ -705,11 +654,6 @@ int register_android_graphics_BitmapFactory(JNIEnv* env) { gBitmap_ninePatchInsetsFieldID = GetFieldIDOrDie(env, bitmap_class, "mNinePatchInsets", "Landroid/graphics/NinePatch$InsetStruct;"); - gInsetStruct_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, - "android/graphics/NinePatch$InsetStruct")); - gInsetStruct_constructorMethodID = GetMethodIDOrDie(env, gInsetStruct_class, "", - "(IIIIIIIIFIF)V"); - gBitmapConfig_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Bitmap$Config")); gBitmapConfig_nativeToConfigMethodID = GetStaticMethodIDOrDie(env, gBitmapConfig_class, diff --git a/core/jni/android/graphics/ByteBufferStreamAdaptor.cpp b/core/jni/android/graphics/ByteBufferStreamAdaptor.cpp new file mode 100644 index 0000000000000..115edd4fe4f42 --- /dev/null +++ b/core/jni/android/graphics/ByteBufferStreamAdaptor.cpp @@ -0,0 +1,323 @@ +#include "ByteBufferStreamAdaptor.h" +#include "core_jni_helpers.h" + +#include + +using namespace android; + +static jmethodID gByteBuffer_getMethodID; +static jmethodID gByteBuffer_setPositionMethodID; + +static JNIEnv* get_env_or_die(JavaVM* jvm) { + JNIEnv* env; + if (jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JNIEnv for JavaVM: %p", jvm); + } + return env; +} + +class ByteBufferStream : public SkStreamAsset { +private: + ByteBufferStream(JavaVM* jvm, jobject jbyteBuffer, size_t initialPosition, size_t length, + jbyteArray storage) + : mJvm(jvm) + , mByteBuffer(jbyteBuffer) + , mPosition(0) + , mInitialPosition(initialPosition) + , mLength(length) + , mStorage(storage) {} + +public: + static ByteBufferStream* Create(JavaVM* jvm, JNIEnv* env, jobject jbyteBuffer, + size_t position, size_t length) { + // This object outlives its native method call. + jbyteBuffer = env->NewGlobalRef(jbyteBuffer); + if (!jbyteBuffer) { + return nullptr; + } + + jbyteArray storage = env->NewByteArray(kStorageSize); + if (!storage) { + env->DeleteGlobalRef(jbyteBuffer); + return nullptr; + } + + // This object outlives its native method call. + storage = static_cast(env->NewGlobalRef(storage)); + if (!storage) { + env->DeleteGlobalRef(jbyteBuffer); + return nullptr; + } + + return new ByteBufferStream(jvm, jbyteBuffer, position, length, storage); + } + + ~ByteBufferStream() override { + auto* env = get_env_or_die(mJvm); + env->DeleteGlobalRef(mByteBuffer); + env->DeleteGlobalRef(mStorage); + } + + size_t read(void* buffer, size_t size) override { + if (size > mLength - mPosition) { + size = mLength - mPosition; + } + if (!size) { + return 0; + } + + if (!buffer) { + return this->setPosition(mPosition + size); + } + + auto* env = get_env_or_die(mJvm); + size_t bytesRead = 0; + do { + const size_t requested = (size > kStorageSize) ? kStorageSize : size; + const jint jrequested = static_cast(requested); + env->CallObjectMethod(mByteBuffer, gByteBuffer_getMethodID, mStorage, 0, jrequested); + if (env->ExceptionCheck()) { + ALOGE("Error in ByteBufferStream::read - was the ByteBuffer modified externally?"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return bytesRead; + } + + env->GetByteArrayRegion(mStorage, 0, requested, reinterpret_cast(buffer)); + if (env->ExceptionCheck()) { + ALOGE("Internal error in ByteBufferStream::read"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return bytesRead; + } + + mPosition += requested; + buffer = reinterpret_cast(reinterpret_cast(buffer) + requested); + bytesRead += requested; + size -= requested; + } while (size); + return bytesRead; + } + + bool isAtEnd() const override { return mLength == mPosition; } + + // SkStreamRewindable overrides + bool rewind() override { return this->setPosition(0); } + + SkStreamAsset* onDuplicate() const override { + // SkStreamRewindable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. A proper + // implementation would require duplicating the ByteBuffer, which has + // its own internal position state. + return nullptr; + } + + // SkStreamSeekable overrides + size_t getPosition() const override { return mPosition; } + + bool seek(size_t position) override { + return this->setPosition(position > mLength ? mLength : position); + } + + bool move(long offset) override { + long newPosition = mPosition + offset; + if (newPosition < 0) { + return this->setPosition(0); + } + return this->seek(static_cast(newPosition)); + } + + SkStreamAsset* onFork() const override { + // SkStreamSeekable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. A proper + // implementation would require duplicating the ByteBuffer, which has + // its own internal position state. + return nullptr; + } + + // SkStreamAsset overrides + size_t getLength() const override { return mLength; } + +private: + JavaVM* mJvm; + jobject mByteBuffer; + // Logical position of the SkStream, between 0 and mLength. + size_t mPosition; + // Initial position of mByteBuffer, treated as mPosition 0. + const size_t mInitialPosition; + // Logical length of the SkStream, from mInitialPosition to + // mByteBuffer.limit(). + const size_t mLength; + + // Range has already been checked by the caller. + bool setPosition(size_t newPosition) { + auto* env = get_env_or_die(mJvm); + env->CallObjectMethod(mByteBuffer, gByteBuffer_setPositionMethodID, + newPosition + mInitialPosition); + if (env->ExceptionCheck()) { + ALOGE("Internal error in ByteBufferStream::setPosition"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return false; + } + mPosition = newPosition; + return true; + } + + // FIXME: This is an arbitrary storage size, which should be plenty for + // some formats (png, gif, many bmps). But for jpeg, the more we can supply + // in one call the better, and webp really wants all of the data. How to + // best choose the amount of storage used? + static constexpr size_t kStorageSize = 4096; + jbyteArray mStorage; +}; + +class ByteArrayStream : public SkStreamAsset { +private: + ByteArrayStream(JavaVM* jvm, jbyteArray jarray, size_t offset, size_t length) + : mJvm(jvm), mByteArray(jarray), mOffset(offset), mPosition(0), mLength(length) {} + +public: + static ByteArrayStream* Create(JavaVM* jvm, JNIEnv* env, jbyteArray jarray, size_t offset, + size_t length) { + // This object outlives its native method call. + jarray = static_cast(env->NewGlobalRef(jarray)); + if (!jarray) { + return nullptr; + } + return new ByteArrayStream(jvm, jarray, offset, length); + } + + ~ByteArrayStream() override { + auto* env = get_env_or_die(mJvm); + env->DeleteGlobalRef(mByteArray); + } + + size_t read(void* buffer, size_t size) override { + if (size > mLength - mPosition) { + size = mLength - mPosition; + } + if (!size) { + return 0; + } + + auto* env = get_env_or_die(mJvm); + if (buffer) { + env->GetByteArrayRegion(mByteArray, mPosition + mOffset, size, + reinterpret_cast(buffer)); + if (env->ExceptionCheck()) { + ALOGE("Internal error in ByteArrayStream::read"); + env->ExceptionDescribe(); + env->ExceptionClear(); + mPosition = mLength; + return 0; + } + } + + mPosition += size; + return size; + } + + bool isAtEnd() const override { return mLength == mPosition; } + + // SkStreamRewindable overrides + bool rewind() override { + mPosition = 0; + return true; + } + SkStreamAsset* onDuplicate() const override { + // SkStreamRewindable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. Note that a proper + // implementation is fairly straightforward + return nullptr; + } + + // SkStreamSeekable overrides + size_t getPosition() const override { return mPosition; } + + bool seek(size_t position) override { + mPosition = (position > mLength) ? mLength : position; + return true; + } + + bool move(long offset) override { + long newPosition = mPosition + offset; + if (newPosition < 0) { + return this->seek(0); + } + return this->seek(static_cast(newPosition)); + } + + SkStreamAsset* onFork() const override { + // SkStreamSeekable requires overriding this, but it is not called by + // decoders, so does not need a true implementation. Note that a proper + // implementation is fairly straightforward + return nullptr; + } + + // SkStreamAsset overrides + size_t getLength() const override { return mLength; } + +private: + JavaVM* mJvm; + jbyteArray mByteArray; + // Offset in mByteArray. Only used when communicating with Java. + const size_t mOffset; + // Logical position of the SkStream, between 0 and mLength. + size_t mPosition; + const size_t mLength; +}; + +struct release_proc_context { + JavaVM* jvm; + jobject jbyteBuffer; +}; + +std::unique_ptr CreateByteBufferStreamAdaptor(JNIEnv* env, jobject jbyteBuffer, + size_t position, size_t limit) { + JavaVM* jvm; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&jvm) != JNI_OK); + + const size_t length = limit - position; + void* addr = env->GetDirectBufferAddress(jbyteBuffer); + if (addr) { + addr = reinterpret_cast(reinterpret_cast(addr) + position); + jbyteBuffer = env->NewGlobalRef(jbyteBuffer); + if (!jbyteBuffer) { + return nullptr; + } + + auto* context = new release_proc_context{jvm, jbyteBuffer}; + auto releaseProc = [](const void*, void* context) { + auto* c = reinterpret_cast(context); + JNIEnv* env = get_env_or_die(c->jvm); + env->DeleteGlobalRef(c->jbyteBuffer); + delete c; + }; + auto data = SkData::MakeWithProc(addr, length, releaseProc, context); + // The new SkMemoryStream will read directly from addr. + return std::unique_ptr(new SkMemoryStream(std::move(data))); + } + + // Non-direct, or direct access is not supported. + return std::unique_ptr(ByteBufferStream::Create(jvm, env, jbyteBuffer, position, + length)); +} + +std::unique_ptr CreateByteArrayStreamAdaptor(JNIEnv* env, jbyteArray array, size_t offset, + size_t length) { + JavaVM* jvm; + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&jvm) != JNI_OK); + + return std::unique_ptr(ByteArrayStream::Create(jvm, env, array, offset, length)); +} + +int register_android_graphics_ByteBufferStreamAdaptor(JNIEnv* env) { + jclass byteBuffer_class = FindClassOrDie(env, "java/nio/ByteBuffer"); + gByteBuffer_getMethodID = GetMethodIDOrDie(env, byteBuffer_class, "get", "([BII)Ljava/nio/ByteBuffer;"); + gByteBuffer_setPositionMethodID = GetMethodIDOrDie(env, byteBuffer_class, "position", "(I)Ljava/nio/Buffer;"); + return true; +} diff --git a/core/jni/android/graphics/ByteBufferStreamAdaptor.h b/core/jni/android/graphics/ByteBufferStreamAdaptor.h new file mode 100644 index 0000000000000..367a48fad9b93 --- /dev/null +++ b/core/jni/android/graphics/ByteBufferStreamAdaptor.h @@ -0,0 +1,37 @@ +#ifndef _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_ +#define _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_ + +#include +#include + +class SkStream; + +/** + * Create an adaptor for treating a java.nio.ByteBuffer as an SkStream. + * + * This will special case direct ByteBuffers, but not the case where a byte[] + * can be used directly. For that, use CreateByteArrayStreamAdaptor. + * + * @param jbyteBuffer corresponding to the java ByteBuffer. This method will + * add a global ref. + * @param initialPosition returned by ByteBuffer.position(). Decoding starts + * from here. + * @param limit returned by ByteBuffer.limit(). + * + * Returns null on failure. + */ +std::unique_ptr CreateByteBufferStreamAdaptor(JNIEnv*, jobject jbyteBuffer, + size_t initialPosition, size_t limit); + +/** + * Create an adaptor for treating a Java byte[] as an SkStream. + * + * @param offset into the byte[] of the beginning of the data to use. + * @param length of data to use, starting from offset. + * + * Returns null on failure. + */ +std::unique_ptr CreateByteArrayStreamAdaptor(JNIEnv*, jbyteArray array, size_t offset, + size_t length); + +#endif // _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_ diff --git a/core/jni/android/graphics/ImageDecoder.cpp b/core/jni/android/graphics/ImageDecoder.cpp new file mode 100644 index 0000000000000..bacab2a304cce --- /dev/null +++ b/core/jni/android/graphics/ImageDecoder.cpp @@ -0,0 +1,473 @@ +/* + * 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. + */ + +#include "Bitmap.h" +#include "ByteBufferStreamAdaptor.h" +#include "GraphicsJNI.h" +#include "NinePatchPeeker.h" +#include "Utils.h" +#include "core_jni_helpers.h" + +#include +#include + +#include +#include +#include + +#include +#include + +using namespace android; + +static jclass gImageDecoder_class; +static jclass gPoint_class; +static jclass gIncomplete_class; +static jclass gCorrupt_class; +static jclass gCanvas_class; +static jmethodID gImageDecoder_constructorMethodID; +static jmethodID gPoint_constructorMethodID; +static jmethodID gIncomplete_constructorMethodID; +static jmethodID gCorrupt_constructorMethodID; +static jmethodID gCallback_onExceptionMethodID; +static jmethodID gPostProcess_postProcessMethodID; +static jmethodID gCanvas_constructorMethodID; +static jmethodID gCanvas_releaseMethodID; + +struct ImageDecoder { + // These need to stay in sync with ImageDecoder.java's Allocator constants. + enum Allocator { + kDefault_Allocator = 0, + kSoftware_Allocator = 1, + kSharedMemory_Allocator = 2, + kHardware_Allocator = 3, + }; + + // These need to stay in sync with PixelFormat.java's Format constants. + enum PixelFormat { + kUnknown = 0, + kTranslucent = -3, + kOpaque = -1, + }; + + std::unique_ptr mCodec; + NinePatchPeeker mPeeker; +}; + +static jobject native_create(JNIEnv* env, std::unique_ptr stream) { + if (!stream.get()) { + return nullObjectReturn("Failed to create a stream"); + } + std::unique_ptr decoder(new ImageDecoder); + decoder->mCodec = SkAndroidCodec::MakeFromStream(std::move(stream), &decoder->mPeeker); + if (!decoder->mCodec.get()) { + // FIXME: Add an error code to SkAndroidCodec::MakeFromStream, like + // SkCodec? Then this can print a more informative error message. + // (Or we can print one from within SkCodec.) + ALOGE("Failed to create an SkCodec"); + return nullptr; + } + + const auto& info = decoder->mCodec->getInfo(); + const int width = info.width(); + const int height = info.height(); + return env->NewObject(gImageDecoder_class, gImageDecoder_constructorMethodID, + reinterpret_cast(decoder.release()), width, height); +} + +static jobject ImageDecoder_nCreate(JNIEnv* env, jobject /*clazz*/, jlong assetPtr) { + Asset* asset = reinterpret_cast(assetPtr); + std::unique_ptr stream(new AssetStreamAdaptor(asset)); + return native_create(env, std::move(stream)); +} + +static jobject ImageDecoder_nCreateByteBuffer(JNIEnv* env, jobject /*clazz*/, jobject jbyteBuffer, + jint initialPosition, jint limit) { + std::unique_ptr stream = CreateByteBufferStreamAdaptor(env, jbyteBuffer, + initialPosition, limit); + if (!stream) { + return nullptr; + } + return native_create(env, std::move(stream)); +} + +static jobject ImageDecoder_nCreateByteArray(JNIEnv* env, jobject /*clazz*/, jbyteArray byteArray, + jint offset, jint length) { + std::unique_ptr stream(CreateByteArrayStreamAdaptor(env, byteArray, offset, length)); + return native_create(env, std::move(stream)); +} + +static bool supports_any_down_scale(const SkAndroidCodec* codec) { + return codec->getEncodedFormat() == SkEncodedImageFormat::kWEBP; +} + +static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jobject jcallback, jobject jpostProcess, + jint desiredWidth, jint desiredHeight, jobject jsubset, + jboolean requireMutable, jint allocator, + jboolean requireUnpremul, jboolean preferRamOverQuality, + jboolean asAlphaMask) { + auto* decoder = reinterpret_cast(nativePtr); + SkAndroidCodec* codec = decoder->mCodec.get(); + SkImageInfo decodeInfo = codec->getInfo(); + bool scale = false; + int sampleSize = 1; + if (desiredWidth != decodeInfo.width() || desiredHeight != decodeInfo.height()) { + bool match = false; + if (desiredWidth < decodeInfo.width() && desiredHeight < decodeInfo.height()) { + if (supports_any_down_scale(codec)) { + match = true; + decodeInfo = decodeInfo.makeWH(desiredWidth, desiredHeight); + } else { + int sampleX = decodeInfo.width() / desiredWidth; + int sampleY = decodeInfo.height() / desiredHeight; + sampleSize = std::min(sampleX, sampleY); + SkISize sampledSize = codec->getSampledDimensions(sampleSize); + decodeInfo = decodeInfo.makeWH(sampledSize.width(), sampledSize.height()); + if (decodeInfo.width() == desiredWidth && decodeInfo.height() == desiredHeight) { + match = true; + } + } + } + if (!match) { + scale = true; + if (requireUnpremul && kOpaque_SkAlphaType != decodeInfo.alphaType()) { + doThrowISE(env, "Cannot scale unpremultiplied pixels!"); + return nullptr; + } + } + } + + switch (decodeInfo.alphaType()) { + case kUnpremul_SkAlphaType: + if (!requireUnpremul) { + decodeInfo = decodeInfo.makeAlphaType(kPremul_SkAlphaType); + } + break; + case kPremul_SkAlphaType: + if (requireUnpremul) { + decodeInfo = decodeInfo.makeAlphaType(kUnpremul_SkAlphaType); + } + break; + case kOpaque_SkAlphaType: + break; + case kUnknown_SkAlphaType: + return nullObjectReturn("Unknown alpha type"); + } + + SkColorType colorType = kN32_SkColorType; + if (asAlphaMask && decodeInfo.colorType() == kGray_8_SkColorType) { + // We have to trick Skia to decode this to a single channel. + colorType = kGray_8_SkColorType; + } else if (preferRamOverQuality) { + // FIXME: The post-process might add alpha, which would make a 565 + // result incorrect. If we call the postProcess before now and record + // to a picture, we can know whether alpha was added, and if not, we + // can still use 565. + if (decodeInfo.alphaType() == kOpaque_SkAlphaType && !jpostProcess) { + // If the final result will be hardware, decoding to 565 and then + // uploading to the gpu as 8888 will not save memory. This still + // may save us from using F16, but do not go down to 565. + if (allocator != ImageDecoder::kHardware_Allocator && + (allocator != ImageDecoder::kDefault_Allocator || requireMutable)) { + colorType = kRGB_565_SkColorType; + } + } + // Otherwise, stick with N32 + } else { + // This is currently the only way to know that we should decode to F16. + colorType = codec->computeOutputColorType(colorType); + } + sk_sp colorSpace = codec->computeOutputColorSpace(colorType); + decodeInfo = decodeInfo.makeColorType(colorType).makeColorSpace(colorSpace); + + SkBitmap bm; + auto bitmapInfo = decodeInfo; + if (asAlphaMask && colorType == kGray_8_SkColorType) { + bitmapInfo = bitmapInfo.makeColorType(kAlpha_8_SkColorType); + } + if (!bm.setInfo(bitmapInfo)) { + return nullObjectReturn("Failed to setInfo properly"); + } + + sk_sp nativeBitmap; + // If we are going to scale or subset, we will create a new bitmap later on, + // so use the heap for the temporary. + // FIXME: Use scanline decoding on only a couple lines to save memory. b/70709380. + if (allocator == ImageDecoder::kSharedMemory_Allocator && !scale && !jsubset) { + nativeBitmap = Bitmap::allocateAshmemBitmap(&bm); + } else { + nativeBitmap = Bitmap::allocateHeapBitmap(&bm); + } + if (!nativeBitmap) { + ALOGE("OOM allocating Bitmap with dimensions %i x %i", + decodeInfo.width(), decodeInfo.height()); + doThrowOOME(env); + return nullptr; + } + + jobject jexception = nullptr; + SkAndroidCodec::AndroidOptions options; + options.fSampleSize = sampleSize; + auto result = codec->getAndroidPixels(decodeInfo, bm.getPixels(), bm.rowBytes(), &options); + switch (result) { + case SkCodec::kSuccess: + break; + case SkCodec::kIncompleteInput: + if (jcallback) { + jexception = env->NewObject(gIncomplete_class, gIncomplete_constructorMethodID); + } + break; + case SkCodec::kErrorInInput: + if (jcallback) { + jexception = env->NewObject(gCorrupt_class, gCorrupt_constructorMethodID); + } + break; + default: + ALOGE("getPixels failed with error %i", result); + return nullptr; + } + + if (jexception) { + if (!env->CallBooleanMethod(jcallback, gCallback_onExceptionMethodID, jexception) || + env->ExceptionCheck()) { + return nullptr; + } + } + + float scaleX = 1.0f; + float scaleY = 1.0f; + if (scale) { + scaleX = (float) desiredWidth / decodeInfo.width(); + scaleY = (float) desiredHeight / decodeInfo.height(); + } + + jbyteArray ninePatchChunk = nullptr; + jobject ninePatchInsets = nullptr; + + // Ignore ninepatch when post-processing. + if (!jpostProcess) { + // FIXME: Share more code with BitmapFactory.cpp. + if (decoder->mPeeker.mPatch != nullptr) { + if (scale) { + decoder->mPeeker.scale(scaleX, scaleY, desiredWidth, desiredHeight); + } + size_t ninePatchArraySize = decoder->mPeeker.mPatch->serializedSize(); + ninePatchChunk = env->NewByteArray(ninePatchArraySize); + if (ninePatchChunk == nullptr) { + return nullObjectReturn("ninePatchChunk == null"); + } + + env->SetByteArrayRegion(ninePatchChunk, 0, decoder->mPeeker.mPatchSize, + reinterpret_cast(decoder->mPeeker.mPatch)); + } + + if (decoder->mPeeker.mHasInsets) { + ninePatchInsets = decoder->mPeeker.createNinePatchInsets(env, 1.0f); + if (ninePatchInsets == nullptr) { + return nullObjectReturn("nine patch insets == null"); + } + } + } + + if (scale || jsubset) { + int translateX = 0; + int translateY = 0; + if (jsubset) { + SkIRect subset; + GraphicsJNI::jrect_to_irect(env, jsubset, &subset); + + // FIXME: If there is no scale, should this instead call + // SkBitmap::extractSubset? If we could upload a subset + // (b/70626068), this would save memory and time. Even for a + // software Bitmap, the extra speed might be worth the memory + // tradeoff if the subset is large? + translateX = -subset.fLeft; + translateY = -subset.fTop; + desiredWidth = subset.width(); + desiredHeight = subset.height(); + } + SkImageInfo scaledInfo = bitmapInfo.makeWH(desiredWidth, desiredHeight); + SkBitmap scaledBm; + if (!scaledBm.setInfo(scaledInfo)) { + nullObjectReturn("Failed scaled setInfo"); + } + + sk_sp scaledPixelRef; + if (allocator == ImageDecoder::kSharedMemory_Allocator) { + scaledPixelRef = Bitmap::allocateAshmemBitmap(&scaledBm); + } else { + scaledPixelRef = Bitmap::allocateHeapBitmap(&scaledBm); + } + if (!scaledPixelRef) { + ALOGE("OOM allocating scaled Bitmap with dimensions %i x %i", + desiredWidth, desiredHeight); + doThrowOOME(env); + return nullptr; + } + + SkPaint paint; + paint.setBlendMode(SkBlendMode::kSrc); + paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering + + SkCanvas canvas(scaledBm, SkCanvas::ColorBehavior::kLegacy); + canvas.translate(translateX, translateY); + canvas.scale(scaleX, scaleY); + canvas.drawBitmap(bm, 0.0f, 0.0f, &paint); + + bm.swap(scaledBm); + nativeBitmap = scaledPixelRef; + } + + if (jpostProcess) { + std::unique_ptr canvas(Canvas::create_canvas(bm)); + if (!canvas) { + return nullObjectReturn("Failed to create Canvas for PostProcess!"); + } + jobject jcanvas = env->NewObject(gCanvas_class, gCanvas_constructorMethodID, + reinterpret_cast(canvas.get())); + if (!jcanvas) { + return nullObjectReturn("Failed to create Java Canvas for PostProcess!"); + } + // jcanvas will now own canvas. + canvas.release(); + + jint pixelFormat = env->CallIntMethod(jpostProcess, gPostProcess_postProcessMethodID, + jcanvas, bm.width(), bm.height()); + if (env->ExceptionCheck()) { + return nullptr; + } + + // The Canvas objects are no longer needed, and will not remain valid. + env->CallVoidMethod(jcanvas, gCanvas_releaseMethodID); + if (env->ExceptionCheck()) { + return nullptr; + } + + SkAlphaType newAlphaType = bm.alphaType(); + switch (pixelFormat) { + case ImageDecoder::kUnknown: + break; + case ImageDecoder::kTranslucent: + newAlphaType = kPremul_SkAlphaType; + break; + case ImageDecoder::kOpaque: + newAlphaType = kOpaque_SkAlphaType; + break; + default: + ALOGE("invalid return from postProcess: %i", pixelFormat); + doThrowIAE(env); + return nullptr; + } + + if (newAlphaType != bm.alphaType()) { + if (!bm.setAlphaType(newAlphaType)) { + ALOGE("incompatible return from postProcess: %i", pixelFormat); + doThrowIAE(env); + return nullptr; + } + nativeBitmap->setAlphaType(newAlphaType); + } + } + + int bitmapCreateFlags = 0x0; + if (!requireUnpremul) { + // Even if the image is opaque, setting this flag means that + // if alpha is added (e.g. by PostProcess), it will be marked as + // premultiplied. + bitmapCreateFlags |= bitmap::kBitmapCreateFlag_Premultiplied; + } + + if (requireMutable) { + bitmapCreateFlags |= bitmap::kBitmapCreateFlag_Mutable; + } else { + if ((allocator == ImageDecoder::kDefault_Allocator || + allocator == ImageDecoder::kHardware_Allocator) + && bm.colorType() != kAlpha_8_SkColorType) + { + sk_sp hwBitmap = Bitmap::allocateHardwareBitmap(bm); + if (hwBitmap) { + hwBitmap->setImmutable(); + return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags, + ninePatchChunk, ninePatchInsets); + } + if (allocator == ImageDecoder::kHardware_Allocator) { + return nullObjectReturn("failed to allocate hardware Bitmap!"); + } + // If we failed to create a hardware bitmap, go ahead and create a + // software one. + } + + nativeBitmap->setImmutable(); + } + return bitmap::createBitmap(env, nativeBitmap.release(), bitmapCreateFlags, ninePatchChunk, + ninePatchInsets); +} + +static jobject ImageDecoder_nGetSampledSize(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jint sampleSize) { + auto* decoder = reinterpret_cast(nativePtr); + SkISize size = decoder->mCodec->getSampledDimensions(sampleSize); + return env->NewObject(gPoint_class, gPoint_constructorMethodID, size.width(), size.height()); +} + +static void ImageDecoder_nGetPadding(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, + jobject outPadding) { + auto* decoder = reinterpret_cast(nativePtr); + decoder->mPeeker.getPadding(env, outPadding); +} + +static void ImageDecoder_nRecycle(JNIEnv* /*env*/, jobject /*clazz*/, jlong nativePtr) { + delete reinterpret_cast(nativePtr); +} + +static const JNINativeMethod gImageDecoderMethods[] = { + { "nCreate", "(J)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreate }, + { "nCreate", "(Ljava/nio/ByteBuffer;II)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateByteBuffer }, + { "nCreate", "([BII)Landroid/graphics/ImageDecoder;", (void*) ImageDecoder_nCreateByteArray }, + { "nDecodeBitmap", "(JLandroid/graphics/ImageDecoder$OnExceptionListener;Landroid/graphics/PostProcess;IILandroid/graphics/Rect;ZIZZZ)Landroid/graphics/Bitmap;", + (void*) ImageDecoder_nDecodeBitmap }, + { "nGetSampledSize","(JI)Landroid/graphics/Point;", (void*) ImageDecoder_nGetSampledSize }, + { "nGetPadding", "(JLandroid/graphics/Rect;)V", (void*) ImageDecoder_nGetPadding }, + { "nRecycle", "(J)V", (void*) ImageDecoder_nRecycle}, +}; + +int register_android_graphics_ImageDecoder(JNIEnv* env) { + gImageDecoder_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder")); + gImageDecoder_constructorMethodID = GetMethodIDOrDie(env, gImageDecoder_class, "", "(JII)V"); + + gPoint_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Point")); + gPoint_constructorMethodID = GetMethodIDOrDie(env, gPoint_class, "", "(II)V"); + + gIncomplete_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder$IncompleteException")); + gIncomplete_constructorMethodID = GetMethodIDOrDie(env, gIncomplete_class, "", "()V"); + + gCorrupt_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder$CorruptException")); + gCorrupt_constructorMethodID = GetMethodIDOrDie(env, gCorrupt_class, "", "()V"); + + jclass callback_class = FindClassOrDie(env, "android/graphics/ImageDecoder$OnExceptionListener"); + gCallback_onExceptionMethodID = GetMethodIDOrDie(env, callback_class, "onException", "(Ljava/lang/Exception;)Z"); + + jclass postProcess_class = FindClassOrDie(env, "android/graphics/PostProcess"); + gPostProcess_postProcessMethodID = GetMethodIDOrDie(env, postProcess_class, "postProcess", "(Landroid/graphics/Canvas;II)I"); + + gCanvas_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Canvas")); + gCanvas_constructorMethodID = GetMethodIDOrDie(env, gCanvas_class, "", "(J)V"); + gCanvas_releaseMethodID = GetMethodIDOrDie(env, gCanvas_class, "release", "()V"); + + return android::RegisterMethodsOrDie(env, "android/graphics/ImageDecoder", gImageDecoderMethods, + NELEM(gImageDecoderMethods)); +} diff --git a/core/jni/android/graphics/NinePatch.cpp b/core/jni/android/graphics/NinePatch.cpp index 564afeb394901..261910727db94 100644 --- a/core/jni/android/graphics/NinePatch.cpp +++ b/core/jni/android/graphics/NinePatch.cpp @@ -29,11 +29,15 @@ #include "SkLatticeIter.h" #include "SkRegion.h" #include "GraphicsJNI.h" +#include "NinePatchPeeker.h" #include "NinePatchUtils.h" #include #include "core_jni_helpers.h" +jclass gInsetStruct_class; +jmethodID gInsetStruct_constructorMethodID; + using namespace android; /** @@ -128,6 +132,30 @@ public: }; +jobject NinePatchPeeker::createNinePatchInsets(JNIEnv* env, float scale) const { + if (!mHasInsets) { + return nullptr; + } + + return env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID, + mOpticalInsets[0], mOpticalInsets[1], + mOpticalInsets[2], mOpticalInsets[3], + mOutlineInsets[0], mOutlineInsets[1], + mOutlineInsets[2], mOutlineInsets[3], + mOutlineRadius, mOutlineAlpha, scale); +} + +void NinePatchPeeker::getPadding(JNIEnv* env, jobject outPadding) const { + if (mPatch) { + GraphicsJNI::set_jrect(env, outPadding, + mPatch->paddingLeft, mPatch->paddingTop, + mPatch->paddingRight, mPatch->paddingBottom); + + } else { + GraphicsJNI::set_jrect(env, outPadding, -1, -1, -1, -1); + } +} + ///////////////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gNinePatchMethods[] = { @@ -140,6 +168,10 @@ static const JNINativeMethod gNinePatchMethods[] = { }; int register_android_graphics_NinePatch(JNIEnv* env) { + gInsetStruct_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, + "android/graphics/NinePatch$InsetStruct")); + gInsetStruct_constructorMethodID = GetMethodIDOrDie(env, gInsetStruct_class, "", + "(IIIIIIIIFIF)V"); return android::RegisterMethodsOrDie(env, "android/graphics/NinePatch", gNinePatchMethods, NELEM(gNinePatchMethods)); } diff --git a/core/jni/android/graphics/NinePatchPeeker.cpp b/core/jni/android/graphics/NinePatchPeeker.cpp index 1ea5650bc20b2..066d47b4c108a 100644 --- a/core/jni/android/graphics/NinePatchPeeker.cpp +++ b/core/jni/android/graphics/NinePatchPeeker.cpp @@ -16,7 +16,8 @@ #include "NinePatchPeeker.h" -#include "SkBitmap.h" +#include +#include using namespace android; @@ -46,3 +47,42 @@ bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t lengt } return true; // keep on decoding } + +static void scaleDivRange(int32_t* divs, int count, float scale, int maxValue) { + for (int i = 0; i < count; i++) { + divs[i] = int32_t(divs[i] * scale + 0.5f); + if (i > 0 && divs[i] == divs[i - 1]) { + divs[i]++; // avoid collisions + } + } + + if (CC_UNLIKELY(divs[count - 1] > maxValue)) { + // if the collision avoidance above put some divs outside the bounds of the bitmap, + // slide outer stretchable divs inward to stay within bounds + int highestAvailable = maxValue; + for (int i = count - 1; i >= 0; i--) { + divs[i] = highestAvailable; + if (i > 0 && divs[i] <= divs[i-1]) { + // keep shifting + highestAvailable = divs[i] - 1; + } else { + break; + } + } + } +} + +void NinePatchPeeker::scale(float scaleX, float scaleY, int scaledWidth, int scaledHeight) { + if (!mPatch) { + return; + } + mPatch->paddingLeft = int(mPatch->paddingLeft * scaleX + 0.5f); + mPatch->paddingTop = int(mPatch->paddingTop * scaleY + 0.5f); + mPatch->paddingRight = int(mPatch->paddingRight * scaleX + 0.5f); + mPatch->paddingBottom = int(mPatch->paddingBottom * scaleY + 0.5f); + + // The max value for the divRange is one pixel less than the actual max to ensure that the size + // of the last div is not zero. A div of size 0 is considered invalid input and will not render. + scaleDivRange(mPatch->getXDivs(), mPatch->numXDivs, scaleX, scaledWidth - 1); + scaleDivRange(mPatch->getYDivs(), mPatch->numYDivs, scaleY, scaledHeight - 1); +} diff --git a/core/jni/android/graphics/NinePatchPeeker.h b/core/jni/android/graphics/NinePatchPeeker.h index 126eab25fc303..e4e58dda4783b 100644 --- a/core/jni/android/graphics/NinePatchPeeker.h +++ b/core/jni/android/graphics/NinePatchPeeker.h @@ -20,7 +20,7 @@ #include "SkPngChunkReader.h" #include -class SkImageDecoder; +#include using namespace android; @@ -42,9 +42,14 @@ public: bool readChunk(const char tag[], const void* data, size_t length) override; + jobject createNinePatchInsets(JNIEnv*, float scale) const; + void getPadding(JNIEnv*, jobject outPadding) const; + void scale(float scaleX, float scaleY, int scaledWidth, int scaledHeight); + Res_png_9patch* mPatch; size_t mPatchSize; bool mHasInsets; +private: int32_t mOpticalInsets[4]; int32_t mOutlineInsets[4]; float mOutlineRadius; diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java new file mode 100644 index 0000000000000..60416a7202310 --- /dev/null +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -0,0 +1,665 @@ +/* + * 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; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RawRes; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.NinePatchDrawable; + +import java.nio.ByteBuffer; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ArrayIndexOutOfBoundsException; +import java.lang.NullPointerException; +import java.lang.RuntimeException; +import java.lang.annotation.Retention; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * Class for decoding images as {@link Bitmap}s or {@link Drawable}s. + * @hide + */ +public final class ImageDecoder { + /** + * Source of the encoded image data. + */ + public static abstract class Source { + /* @hide */ + Resources getResources() { return null; } + + /* @hide */ + void close() {} + + /* @hide */ + abstract ImageDecoder createImageDecoder(); + }; + + private static class ByteArraySource extends Source { + ByteArraySource(byte[] data, int offset, int length) { + mData = data; + mOffset = offset; + mLength = length; + }; + private final byte[] mData; + private final int mOffset; + private final int mLength; + + @Override + public ImageDecoder createImageDecoder() { + return nCreate(mData, mOffset, mLength); + } + } + + private static class ByteBufferSource extends Source { + ByteBufferSource(ByteBuffer buffer) { + mBuffer = buffer; + } + private final ByteBuffer mBuffer; + + @Override + public ImageDecoder createImageDecoder() { + if (!mBuffer.isDirect() && mBuffer.hasArray()) { + int offset = mBuffer.arrayOffset() + mBuffer.position(); + int length = mBuffer.limit() - mBuffer.position(); + return nCreate(mBuffer.array(), offset, length); + } + return nCreate(mBuffer, mBuffer.position(), mBuffer.limit()); + } + } + + private static class ResourceSource extends Source { + ResourceSource(Resources res, int resId) + throws Resources.NotFoundException { + // Test that the resource can be found. + InputStream is = null; + try { + is = res.openRawResource(resId); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + + mResources = res; + mResId = resId; + } + + final Resources mResources; + final int mResId; + // This is just stored here in order to keep the underlying Asset + // alive. FIXME: Can I access the Asset (and keep it alive) without + // this object? + InputStream mInputStream; + + @Override + public Resources getResources() { return mResources; } + + @Override + public ImageDecoder createImageDecoder() { + // FIXME: Can I bypass creating the stream? + try { + mInputStream = mResources.openRawResource(mResId); + } catch (Resources.NotFoundException e) { + // This should never happen, since we already tested in the + // constructor. + } + if (!(mInputStream instanceof AssetManager.AssetInputStream)) { + // This should never happen. + throw new RuntimeException("Resource is not an asset?"); + } + long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset(); + return nCreate(asset); + } + + @Override + public void close() { + try { + mInputStream.close(); + } catch (IOException e) { + } finally { + mInputStream = null; + } + } + } + + /** + * Contains information about the encoded image. + */ + public static class ImageInfo { + public final int width; + public final int height; + // TODO?: Add more info? mimetype, ninepatch etc? + + ImageInfo(int width, int height) { + this.width = width; + this.height = height; + } + }; + + /** + * Used if the provided data is incomplete. + * + * There may be a partial image to display. + */ + public class IncompleteException extends Exception {}; + + /** + * Used if the provided data is corrupt. + * + * There may be a partial image to display. + */ + public class CorruptException extends Exception {}; + + /** + * Optional listener supplied to {@link #decodeDrawable} or + * {@link #decodeBitmap}. + */ + public static interface OnHeaderDecodedListener { + /** + * Called when the header is decoded and the size is known. + * + * @param info Information about the encoded image. + * @param decoder allows changing the default settings of the decode. + */ + public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder); + + }; + + /** + * Optional listener supplied to the ImageDecoder. + */ + public static interface OnExceptionListener { + /** + * Called when there is a problem in the stream or in the data. + * FIXME: Or do not allow streams? + * FIXME: Report how much of the image has been decoded? + * + * @param e Exception containing information about the error. + * @return True to create and return a {@link Drawable}/ + * {@link Bitmap} with partial data. False to return + * {@code null}. True is the default. + */ + public boolean onException(Exception e); + }; + + // Fields + private long mNativePtr; + private final int mWidth; + private final int mHeight; + + private int mDesiredWidth; + private int mDesiredHeight; + private int mAllocator = DEFAULT_ALLOCATOR; + private boolean mRequireUnpremultiplied = false; + private boolean mMutable = false; + private boolean mPreferRamOverQuality = false; + private boolean mAsAlphaMask = false; + private Rect mCropRect; + + private PostProcess mPostProcess; + private OnExceptionListener mOnExceptionListener; + + + /** + * Private constructor called by JNI. {@link #recycle} must be + * called after decoding to delete native resources. + */ + @SuppressWarnings("unused") + private ImageDecoder(long nativePtr, int width, int height) { + mNativePtr = nativePtr; + mWidth = width; + mHeight = height; + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Create a new {@link Source} from an asset. + * + * @param res the {@link Resources} object containing the image data. + * @param resId resource ID of the image data. + * // FIXME: Can be an @DrawableRes? + * @return a new Source object, which can be passed to + * {@link #decodeDrawable} or {@link #decodeBitmap}. + * @throws Resources.NotFoundException if the asset does not exist. + */ + public static Source createSource(@NonNull Resources res, @RawRes int resId) + throws Resources.NotFoundException { + return new ResourceSource(res, resId); + } + + /** + * Create a new {@link Source} from a byte array. + * @param data byte array of compressed image data. + * @param offset offset into data for where the decoder should begin + * parsing. + * @param length number of bytes, beginning at offset, to parse. + * @throws NullPointerException if data is null. + * @throws ArrayIndexOutOfBoundsException if offset and length are + * not within data. + */ + // TODO: Overloads that don't use offset, length + public static Source createSource(@NonNull byte[] data, int offset, + int length) throws ArrayIndexOutOfBoundsException { + if (data == null) { + throw new NullPointerException("null byte[] in createSource!"); + } + if (offset < 0 || length < 0 || offset >= data.length || + offset + length > data.length) { + throw new ArrayIndexOutOfBoundsException( + "invalid offset/length!"); + } + return new ByteArraySource(data, offset, length); + } + + /** + * Create a new {@link Source} from a {@link java.nio.ByteBuffer}. + * + * The returned {@link Source} effectively takes ownership of the + * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after + * this call. + * + * Decoding will start from {@link java.nio.ByteBuffer#position()}. + */ + public static Source createSource(ByteBuffer buffer) { + return new ByteBufferSource(buffer); + } + + /** + * Return the width and height of a given sample size. + * + * This takes an input that functions like + * {@link BitmapFactory.Options#inSampleSize}. It returns a width and + * height that can be acheived by sampling the encoded image. Other widths + * and heights may be supported, but will require an additional (internal) + * scaling step. Such internal scaling is *not* supported with + * {@link #requireUnpremultiplied}. + * + * @param sampleSize Sampling rate of the encoded image. + * @return Point {@link Point#x} and {@link Point#y} correspond to the + * width and height after sampling. + */ + public Point getSampledSize(int sampleSize) { + if (sampleSize <= 0) { + throw new IllegalArgumentException("sampleSize must be positive! " + + "provided " + sampleSize); + } + if (mNativePtr == 0) { + throw new IllegalStateException("ImageDecoder is recycled!"); + } + + return nGetSampledSize(mNativePtr, sampleSize); + } + + // Modifiers + /** + * Resize the output to have the following size. + * + * @param width must be greater than 0. + * @param height must be greater than 0. + */ + public void resize(int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Dimensions must be positive! " + + "provided (" + width + ", " + height + ")"); + } + + mDesiredWidth = width; + mDesiredHeight = height; + } + + /** + * Resize based on a sample size. + * + * This has the same effect as passing the result of + * {@link #getSampledSize} to {@link #resize(int, int)}. + * + * @param sampleSize Sampling rate of the encoded image. + */ + public void resize(int sampleSize) { + Point dimensions = this.getSampledSize(sampleSize); + this.resize(dimensions.x, dimensions.y); + } + + // These need to stay in sync with ImageDecoder.cpp's Allocator enum. + /** + * Use the default allocation for the pixel memory. + * + * Will typically result in a {@link Bitmap.Config#HARDWARE} + * allocation, but may be software for small images. In addition, this will + * switch to software when HARDWARE is incompatible, e.g. + * {@link #setMutable}, {@link #setAsAlphaMask}. + */ + public static final int DEFAULT_ALLOCATOR = 0; + + /** + * Use a software allocation for the pixel memory. + * + * Useful for drawing to a software {@link Canvas} or for + * accessing the pixels on the final output. + */ + public static final int SOFTWARE_ALLOCATOR = 1; + + /** + * Use shared memory for the pixel memory. + * + * Useful for sharing across processes. + */ + public static final int SHARED_MEMORY_ALLOCATOR = 2; + + /** + * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}. + * + * This will throw an {@link java.lang.IllegalStateException} when combined + * with incompatible options, like {@link #setMutable} or + * {@link #setAsAlphaMask}. + */ + public static final int HARDWARE_ALLOCATOR = 3; + + /** @hide **/ + @Retention(SOURCE) + @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR, + HARDWARE_ALLOCATOR }) + public @interface Allocator {}; + + /** + * Choose the backing for the pixel memory. + * + * This is ignored for animated drawables. + * + * TODO: Allow accessing the backing from the Bitmap. + * + * @param allocator Type of allocator to use. + */ + public void setAllocator(@Allocator int allocator) { + if (allocator < DEFAULT_ALLOCATOR || allocator > HARDWARE_ALLOCATOR) { + throw new IllegalArgumentException("invalid allocator " + allocator); + } + mAllocator = allocator; + } + + /** + * Create a {@link Bitmap} with unpremultiplied pixels. + * + * By default, ImageDecoder will create a {@link Bitmap} with + * premultiplied pixels, which is required for drawing with the + * {@link android.view.View} system (i.e. to a {@link Canvas}). Calling + * this method will result in {@link #decodeBitmap} returning a + * {@link Bitmap} with unpremultiplied pixels. See + * {@link Bitmap#isPremultiplied}. Incompatible with + * {@link #decodeDrawable}; attempting to decode an unpremultiplied + * {@link Drawable} will throw an {@link java.lang.IllegalStateException}. + */ + public void requireUnpremultiplied() { + mRequireUnpremultiplied = true; + } + + /** + * Modify the image after decoding and scaling. + * + * This allows adding effects prior to returning a {@link Drawable} or + * {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap}, + * this is the only way to process the image after decoding. + * + * If set on a nine-patch image, the nine-patch data is ignored. + * + * For an animated image, the drawing commands drawn on the {@link Canvas} + * will be recorded immediately and then applied to each frame. + */ + public void setPostProcess(PostProcess p) { + mPostProcess = p; + } + + /** + * Set (replace) the {@link OnExceptionListener} on this object. + * + * Will be called if there is an error in the input. Without one, a + * partial {@link Bitmap} will be created. + */ + public void setOnExceptionListener(OnExceptionListener l) { + mOnExceptionListener = l; + } + + /** + * Crop the output to {@code subset} of the (possibly) scaled image. + * + * {@code subset} must be contained within the size set by {@link #resize} + * or the bounds of the image if resize was not called. Otherwise an + * {@link IllegalStateException} will be thrown. + * + * NOT intended as a replacement for + * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats, + * but merely crops the output. + */ + public void crop(Rect subset) { + mCropRect = subset; + } + + /** + * Create a mutable {@link Bitmap}. + * + * By default, a {@link Bitmap} created will be immutable, but that can be + * changed with this call. + * + * Incompatible with {@link #HARDWARE_ALLOCATOR}, because + * {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable. Attempting to + * combine them will throw an {@link java.lang.IllegalStateException}. + * + * Incompatible with {@link #decodeDrawable}, which would require + * retrieving the Bitmap from the returned Drawable in order to modify. + * Attempting to decode a mutable {@link Drawable} will throw an + * {@link java.lang.IllegalStateException} + */ + public void setMutable() { + mMutable = true; + } + + /** + * Potentially save RAM at the expense of quality. + * + * This may result in a {@link Bitmap} with a denser {@link Bitmap.Config}, + * depending on the image. For example, for an opaque {@link Bitmap}, this + * may result in a {@link Bitmap.Config} with no alpha information. + */ + public void setPreferRamOverQuality() { + mPreferRamOverQuality = true; + } + + /** + * Potentially treat the output as an alpha mask. + * + * If the image is encoded in a format with only one channel, treat that + * channel as alpha. Otherwise this call has no effect. + * + * Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them + * will throw an {@link java.lang.IllegalStateException}. + */ + public void setAsAlphaMask() { + mAsAlphaMask = true; + } + + /** + * Clean up resources. + * + * ImageDecoder has a private constructor, and will always be recycled + * by decodeDrawable or decodeBitmap which creates it, so there is no + * need for a finalizer. + */ + private void recycle() { + if (mNativePtr == 0) { + return; + } + nRecycle(mNativePtr); + mNativePtr = 0; + } + + private void checkState() { + if (mNativePtr == 0) { + throw new IllegalStateException("Cannot reuse ImageDecoder.Source!"); + } + + checkSubset(mDesiredWidth, mDesiredHeight, mCropRect); + + if (mAllocator == HARDWARE_ALLOCATOR) { + if (mMutable) { + throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!"); + } + if (mAsAlphaMask) { + throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!"); + } + } + + if (mPostProcess != null && mRequireUnpremultiplied) { + throw new IllegalStateException("Cannot draw to unpremultiplied pixels!"); + } + } + + private static void checkSubset(int width, int height, Rect r) { + if (r == null) { + return; + } + if (r.left < 0 || r.top < 0 || r.right > width || r.bottom > height) { + throw new IllegalStateException("Subset " + r + " not contained by " + + "scaled image bounds: (" + width + " x " + height + ")"); + } + } + + /** + * Create a {@link Drawable}. + */ + public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) { + ImageDecoder decoder = src.createImageDecoder(); + if (decoder == null) { + return null; + } + + if (listener != null) { + ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight); + listener.onHeaderDecoded(info, decoder); + } + + decoder.checkState(); + + if (decoder.mRequireUnpremultiplied) { + // Though this could be supported (ignored) for opaque images, it + // seems better to always report this error. + throw new IllegalStateException("Cannot decode a Drawable with" + + " unpremultiplied pixels!"); + } + + if (decoder.mMutable) { + throw new IllegalStateException("Cannot decode a mutable Drawable!"); + } + + try { + Bitmap bm = nDecodeBitmap(decoder.mNativePtr, + decoder.mOnExceptionListener, + decoder.mPostProcess, + decoder.mDesiredWidth, decoder.mDesiredHeight, + decoder.mCropRect, + false, // decoder.mMutable + decoder.mAllocator, + false, // decoder.mRequireUnpremultiplied + decoder.mPreferRamOverQuality, + decoder.mAsAlphaMask + ); + if (bm == null) { + return null; + } + + Resources res = src.getResources(); + if (res == null) { + bm.setDensity(Bitmap.DENSITY_NONE); + } + + byte[] np = bm.getNinePatchChunk(); + if (np != null && NinePatch.isNinePatchChunk(np)) { + Rect opticalInsets = new Rect(); + bm.getOpticalInsets(opticalInsets); + Rect padding = new Rect(); + nGetPadding(decoder.mNativePtr, padding); + return new NinePatchDrawable(res, bm, np, padding, + opticalInsets, null); + } + + // TODO: Handle animation. + return new BitmapDrawable(res, bm); + } finally { + decoder.recycle(); + src.close(); + } + } + + /** + * Create a {@link Bitmap}. + */ + public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) { + ImageDecoder decoder = src.createImageDecoder(); + if (decoder == null) { + return null; + } + + if (listener != null) { + ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight); + listener.onHeaderDecoded(info, decoder); + } + + decoder.checkState(); + + try { + return nDecodeBitmap(decoder.mNativePtr, + decoder.mOnExceptionListener, + decoder.mPostProcess, + decoder.mDesiredWidth, decoder.mDesiredHeight, + decoder.mCropRect, + decoder.mMutable, + decoder.mAllocator, + decoder.mRequireUnpremultiplied, + decoder.mPreferRamOverQuality, + decoder.mAsAlphaMask); + } finally { + decoder.recycle(); + src.close(); + } + } + + private static native ImageDecoder nCreate(long asset); + private static native ImageDecoder nCreate(ByteBuffer buffer, + int position, + int limit); + private static native ImageDecoder nCreate(byte[] data, int offset, + int length); + private static native Bitmap nDecodeBitmap(long nativePtr, + OnExceptionListener listener, + PostProcess postProcess, + int width, int height, + Rect cropRect, boolean mutable, + int allocator, boolean requireUnpremul, + boolean preferRamOverQuality, boolean asAlphaMask); + private static native Point nGetSampledSize(long nativePtr, + int sampleSize); + private static native void nGetPadding(long nativePtr, Rect outRect); + private static native void nRecycle(long nativePtr); +} diff --git a/graphics/java/android/graphics/PostProcess.java b/graphics/java/android/graphics/PostProcess.java new file mode 100644 index 0000000000000..c5a31e828c972 --- /dev/null +++ b/graphics/java/android/graphics/PostProcess.java @@ -0,0 +1,91 @@ +/* + * 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; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.graphics.drawable.Drawable; + + +/** + * Helper interface for adding custom processing to an image. + * + * The image being processed may be a {@link Drawable}, {@link Bitmap} or frame + * of an animated image produced by {@link ImageDecoder}. This is called before + * the requested object is returned. + * + * This custom processing also applies to image types that are otherwise + * immutable, such as {@link Bitmap.Config#HARDWARE}. + * + * On an animated image, the callback will only be called once, but the drawing + * commands will be applied to each frame, as if the {@code Canvas} had been + * returned by {@link Picture#beginRecording}. + * + * Supplied to ImageDecoder via {@link ImageDecoder#setPostProcess}. + * @hide + */ +public interface PostProcess { + /** + * Do any processing after (for example) decoding. + * + * Drawing to the {@link Canvas} will behave as if the initial processing + * (e.g. decoding) already exists in the Canvas. An implementation can draw + * effects on top of this, or it can even draw behind it using + * {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency + * to the corners to achieve rounded corners. That can be done with the + * following code: + * + * + * Path path = new Path(); + * path.setFillType(Path.FillType.INVERSE_EVEN_ODD); + * path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW); + * Paint paint = new Paint(); + * paint.setAntiAlias(true); + * paint.setColor(Color.TRANSPARENT); + * paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); + * canvas.drawPath(path, paint); + * return PixelFormat.TRANSLUCENT; + * + * + * + * @param canvas The {@link Canvas} to draw to. + * @param width Width of {@code canvas}. Anything drawn outside of this + * will be ignored. + * @param height Height of {@code canvas}. Anything drawn outside of this + * will be ignored. + * @return Opacity of the result after drawing. + * {@link PixelFormat#UNKNOWN} means that the implementation did not + * change whether the image has alpha. Return this unless you added + * transparency (e.g. with the code above, in which case you should + * return {@code PixelFormat.TRANSLUCENT}) or you forced the image to + * be opaque (e.g. by drawing everywhere with an opaque color and + * {@code PorterDuff.Mode.DST_OVER}, in which case you should return + * {@code PixelFormat.OPAQUE}). + * {@link PixelFormat#TRANSLUCENT} means that the implementation added + * transparency. This is safe to return even if the image already had + * transparency. This is also safe to return if the result is opaque, + * though it may draw more slowly. + * {@link PixelFormat#OPAQUE} means that the implementation forced the + * image to be opaque. This is safe to return even if the image was + * already opaque. + * {@link PixelFormat#TRANSPARENT} (or any other integer) is not + * allowed, and will result in throwing an + * {@link java.lang.IllegalArgumentException}. + */ + @PixelFormat.Opacity + public int postProcess(@NonNull Canvas canvas, int width, int height); +}