Merge "ImageDecoder (BitmapFactory 2.0)"
This commit is contained in:
committed by
Android (Google) Code Review
commit
d538bbcbdd
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
323
core/jni/android/graphics/ByteBufferStreamAdaptor.cpp
Normal file
323
core/jni/android/graphics/ByteBufferStreamAdaptor.cpp
Normal file
@@ -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;
|
||||
}
|
||||
37
core/jni/android/graphics/ByteBufferStreamAdaptor.h
Normal file
37
core/jni/android/graphics/ByteBufferStreamAdaptor.h
Normal file
@@ -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_
|
||||
473
core/jni/android/graphics/ImageDecoder.cpp
Normal file
473
core/jni/android/graphics/ImageDecoder.cpp
Normal file
@@ -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));
|
||||
}
|
||||
@@ -29,11 +29,15 @@
|
||||
#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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
665
graphics/java/android/graphics/ImageDecoder.java
Normal file
665
graphics/java/android/graphics/ImageDecoder.java
Normal file
@@ -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);
|
||||
}
|
||||
91
graphics/java/android/graphics/PostProcess.java
Normal file
91
graphics/java/android/graphics/PostProcess.java
Normal file
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user