OSDN Git Service

ImageDecoder (BitmapFactory 2.0)
authorLeon Scroggins III <scroggo@google.com>
Fri, 20 Oct 2017 18:08:11 +0000 (14:08 -0400)
committerLeon Scroggins <scroggo@google.com>
Tue, 19 Dec 2017 20:22:17 +0000 (20:22 +0000)
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
core/jni/AndroidRuntime.cpp
core/jni/android/graphics/BitmapFactory.cpp
core/jni/android/graphics/ByteBufferStreamAdaptor.cpp [new file with mode: 0644]
core/jni/android/graphics/ByteBufferStreamAdaptor.h [new file with mode: 0644]
core/jni/android/graphics/ImageDecoder.cpp [new file with mode: 0644]
core/jni/android/graphics/NinePatch.cpp
core/jni/android/graphics/NinePatchPeeker.cpp
core/jni/android/graphics/NinePatchPeeker.h
graphics/java/android/graphics/ImageDecoder.java [new file with mode: 0644]
graphics/java/android/graphics/PostProcess.java [new file with mode: 0644]

index 8df0fb8..297bae2 100644 (file)
@@ -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",
index 9e907bd..9ab73d4 100644 (file)
@@ -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),
index 2e8c27a..79c5a34 100644 (file)
@@ -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<SkStreamRewindable> 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<SkStreamRewindable> 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<SkStreamRewindable> 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, "<init>",
-                                                        "(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 (file)
index 0000000..115edd4
--- /dev/null
@@ -0,0 +1,323 @@
+#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;
+}
diff --git a/core/jni/android/graphics/ByteBufferStreamAdaptor.h b/core/jni/android/graphics/ByteBufferStreamAdaptor.h
new file mode 100644 (file)
index 0000000..367a48f
--- /dev/null
@@ -0,0 +1,37 @@
+#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_
diff --git a/core/jni/android/graphics/ImageDecoder.cpp b/core/jni/android/graphics/ImageDecoder.cpp
new file mode 100644 (file)
index 0000000..bacab2a
--- /dev/null
@@ -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 <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));
+}
index 564afeb..2619107 100644 (file)
 #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;
 
 /**
@@ -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, "<init>",
+            "(IIIIIIIIFIF)V");
     return android::RegisterMethodsOrDie(env,
             "android/graphics/NinePatch", gNinePatchMethods, NELEM(gNinePatchMethods));
 }
index 1ea5650..066d47b 100644 (file)
@@ -16,7 +16,8 @@
 
 #include "NinePatchPeeker.h"
 
-#include "SkBitmap.h"
+#include <SkBitmap.h>
+#include <cutils/compiler.h>
 
 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);
+}
index 126eab2..e4e58dd 100644 (file)
@@ -20,7 +20,7 @@
 #include "SkPngChunkReader.h"
 #include <androidfw/ResourceTypes.h>
 
-class SkImageDecoder;
+#include <jni.h>
 
 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 (file)
index 0000000..60416a7
--- /dev/null
@@ -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 (file)
index 0000000..c5a31e8
--- /dev/null
@@ -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:
+     *
+     *  <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);
+}