"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",
"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",
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);
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),
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),
jfieldID gBitmap_ninePatchInsetsFieldID;
-jclass gInsetStruct_class;
-jmethodID gInsetStruct_constructorMethodID;
-
jclass gBitmapConfig_class;
jmethodID gBitmapConfig_nativeToConfigMethodID;
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)
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();
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");
}
}
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.
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, "<init>",
- "(IIIIIIIIFIF)V");
-
gBitmapConfig_class = MakeGlobalRefOrDie(env, FindClassOrDie(env,
"android/graphics/Bitmap$Config"));
gBitmapConfig_nativeToConfigMethodID = GetStaticMethodIDOrDie(env, gBitmapConfig_class,
--- /dev/null
+#include "ByteBufferStreamAdaptor.h"
+#include "core_jni_helpers.h"
+
+#include <SkStream.h>
+
+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<void**>(&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<jbyteArray>(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<jint>(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<jbyte*>(buffer));
+ if (env->ExceptionCheck()) {
+ ALOGE("Internal error in ByteBufferStream::read");
+ env->ExceptionDescribe();
+ env->ExceptionClear();
+ mPosition = mLength;
+ return bytesRead;
+ }
+
+ mPosition += requested;
+ buffer = reinterpret_cast<void*>(reinterpret_cast<char*>(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<size_t>(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<jbyteArray>(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<jbyte*>(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<size_t>(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<SkStream> 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<void*>(reinterpret_cast<char*>(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<release_proc_context*>(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<SkStream>(new SkMemoryStream(std::move(data)));
+ }
+
+ // Non-direct, or direct access is not supported.
+ return std::unique_ptr<SkStream>(ByteBufferStream::Create(jvm, env, jbyteBuffer, position,
+ length));
+}
+
+std::unique_ptr<SkStream> 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<SkStream>(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;
+}
--- /dev/null
+#ifndef _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_
+#define _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_
+
+#include <jni.h>
+#include <memory>
+
+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<SkStream> 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<SkStream> CreateByteArrayStreamAdaptor(JNIEnv*, jbyteArray array, size_t offset,
+ size_t length);
+
+#endif // _ANDROID_GRAPHICS_BYTE_BUFFER_STREAM_ADAPTOR_H_
--- /dev/null
+/*
+ * 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 <hwui/Bitmap.h>
+#include <hwui/Canvas.h>
+
+#include <SkAndroidCodec.h>
+#include <SkEncodedImageFormat.h>
+#include <SkStream.h>
+
+#include <androidfw/Asset.h>
+#include <jni.h>
+
+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<SkAndroidCodec> mCodec;
+ NinePatchPeeker mPeeker;
+};
+
+static jobject native_create(JNIEnv* env, std::unique_ptr<SkStream> stream) {
+ if (!stream.get()) {
+ return nullObjectReturn("Failed to create a stream");
+ }
+ std::unique_ptr<ImageDecoder> 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<jlong>(decoder.release()), width, height);
+}
+
+static jobject ImageDecoder_nCreate(JNIEnv* env, jobject /*clazz*/, jlong assetPtr) {
+ Asset* asset = reinterpret_cast<Asset*>(assetPtr);
+ std::unique_ptr<SkStream> 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<SkStream> 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<SkStream> 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<ImageDecoder*>(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<SkColorSpace> 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<Bitmap> 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<jbyte*>(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<Bitmap> 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(Canvas::create_canvas(bm));
+ if (!canvas) {
+ return nullObjectReturn("Failed to create Canvas for PostProcess!");
+ }
+ jobject jcanvas = env->NewObject(gCanvas_class, gCanvas_constructorMethodID,
+ reinterpret_cast<jlong>(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<Bitmap> 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<ImageDecoder*>(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<ImageDecoder*>(nativePtr);
+ decoder->mPeeker.getPadding(env, outPadding);
+}
+
+static void ImageDecoder_nRecycle(JNIEnv* /*env*/, jobject /*clazz*/, jlong nativePtr) {
+ delete reinterpret_cast<ImageDecoder*>(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, "<init>", "(JII)V");
+
+ gPoint_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/Point"));
+ gPoint_constructorMethodID = GetMethodIDOrDie(env, gPoint_class, "<init>", "(II)V");
+
+ gIncomplete_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder$IncompleteException"));
+ gIncomplete_constructorMethodID = GetMethodIDOrDie(env, gIncomplete_class, "<init>", "()V");
+
+ gCorrupt_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ImageDecoder$CorruptException"));
+ gCorrupt_constructorMethodID = GetMethodIDOrDie(env, gCorrupt_class, "<init>", "()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, "<init>", "(J)V");
+ gCanvas_releaseMethodID = GetMethodIDOrDie(env, gCanvas_class, "release", "()V");
+
+ return android::RegisterMethodsOrDie(env, "android/graphics/ImageDecoder", gImageDecoderMethods,
+ NELEM(gImageDecoderMethods));
+}
#include "SkLatticeIter.h"
#include "SkRegion.h"
#include "GraphicsJNI.h"
+#include "NinePatchPeeker.h"
#include "NinePatchUtils.h"
#include <nativehelper/JNIHelp.h>
#include "core_jni_helpers.h"
+jclass gInsetStruct_class;
+jmethodID gInsetStruct_constructorMethodID;
+
using namespace android;
/**
};
+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[] = {
};
int register_android_graphics_NinePatch(JNIEnv* env) {
+ gInsetStruct_class = MakeGlobalRefOrDie(env, FindClassOrDie(env,
+ "android/graphics/NinePatch$InsetStruct"));
+ gInsetStruct_constructorMethodID = GetMethodIDOrDie(env, gInsetStruct_class, "<init>",
+ "(IIIIIIIIFIF)V");
return android::RegisterMethodsOrDie(env,
"android/graphics/NinePatch", gNinePatchMethods, NELEM(gNinePatchMethods));
}
#include "NinePatchPeeker.h"
-#include "SkBitmap.h"
+#include <SkBitmap.h>
+#include <cutils/compiler.h>
using namespace android;
}
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);
+}
#include "SkPngChunkReader.h"
#include <androidfw/ResourceTypes.h>
-class SkImageDecoder;
+#include <jni.h>
using namespace android;
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;
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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:
+ *
+ * <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;
+ * </code>
+ *
+ *
+ * @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);
+}