Merge "ImageDecoder (BitmapFactory 2.0)"

This commit is contained in:
TreeHugger Robot
2017-12-20 01:36:18 +00:00
committed by Android (Google) Code Review
11 changed files with 1677 additions and 61 deletions

View 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",

View 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),

View 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,

View 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;
}

View 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_

View 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));
}

View File

@@ -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));
}

View 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);
}

View 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;

View 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);
}

View 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);
}