summaryrefslogtreecommitdiff
path: root/android/graphics/ImageDecoder.java
diff options
context:
space:
mode:
Diffstat (limited to 'android/graphics/ImageDecoder.java')
-rw-r--r--android/graphics/ImageDecoder.java1501
1 files changed, 1351 insertions, 150 deletions
diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java
index 506eab5c..098f1000 100644
--- a/android/graphics/ImageDecoder.java
+++ b/android/graphics/ImageDecoder.java
@@ -16,36 +16,177 @@
package android.graphics;
+import static android.system.OsConstants.SEEK_CUR;
+import static android.system.OsConstants.SEEK_SET;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.AnyThread;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.Px;
+import android.annotation.TestApi;
+import android.annotation.WorkerThread;
import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
import android.content.res.AssetManager.AssetInputStream;
import android.content.res.Resources;
import android.graphics.drawable.AnimatedImageDrawable;
-import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
import android.net.Uri;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
import android.util.DisplayMetrics;
import android.util.Size;
import android.util.TypedValue;
-import java.nio.ByteBuffer;
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoUtils;
+
import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.lang.ArrayIndexOutOfBoundsException;
-import java.lang.AutoCloseable;
-import java.lang.NullPointerException;
import java.lang.annotation.Retention;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
- * Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
+ * <p>A class for converting encoded images (like {@code PNG}, {@code JPEG},
+ * {@code WEBP}, {@code GIF}, or {@code HEIF}) into {@link Drawable} or
+ * {@link Bitmap} objects.
+ *
+ * <p>To use it, first create a {@link Source Source} using one of the
+ * {@code createSource} overloads. For example, to decode from a {@link File}, call
+ * {@link #createSource(File)} and pass the result to {@link #decodeDrawable(Source)}
+ * or {@link #decodeBitmap(Source)}:
+ *
+ * <pre class="prettyprint">
+ * File file = new File(...);
+ * ImageDecoder.Source source = ImageDecoder.createSource(file);
+ * Drawable drawable = ImageDecoder.decodeDrawable(source);
+ * </pre>
+ *
+ * <p>To change the default settings, pass the {@link Source Source} and an
+ * {@link OnHeaderDecodedListener OnHeaderDecodedListener} to
+ * {@link #decodeDrawable(Source, OnHeaderDecodedListener)} or
+ * {@link #decodeBitmap(Source, OnHeaderDecodedListener)}. For example, to
+ * create a sampled image with half the width and height of the original image,
+ * call {@link #setTargetSampleSize setTargetSampleSize(2)} inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}:
+ *
+ * <pre class="prettyprint">
+ * OnHeaderDecodedListener listener = new OnHeaderDecodedListener() {
+ * public void onHeaderDecoded(ImageDecoder decoder, ImageInfo info, Source source) {
+ * decoder.setTargetSampleSize(2);
+ * }
+ * };
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, listener);
+ * </pre>
+ *
+ * <p>The {@link ImageInfo ImageInfo} contains information about the encoded image, like
+ * its width and height, and the {@link Source Source} can be used to match to a particular
+ * {@link Source Source} if a single {@link OnHeaderDecodedListener OnHeaderDecodedListener}
+ * is used with multiple {@link Source Source} objects.
+ *
+ * <p>The {@link OnHeaderDecodedListener OnHeaderDecodedListener} can also be implemented
+ * as a lambda:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ * decoder.setTargetSampleSize(2);
+ * });
+ * </pre>
+ *
+ * <p>If the encoded image is an animated {@code GIF} or {@code WEBP},
+ * {@link #decodeDrawable decodeDrawable} will return an {@link AnimatedImageDrawable}. To
+ * start its animation, call {@link AnimatedImageDrawable#start AnimatedImageDrawable.start()}:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source);
+ * if (drawable instanceof AnimatedImageDrawable) {
+ * ((AnimatedImageDrawable) drawable).start();
+ * }
+ * </pre>
+ *
+ * <p>By default, a {@link Bitmap} created by {@link ImageDecoder} (including
+ * one that is inside a {@link Drawable}) will be immutable (i.e.
+ * {@link Bitmap#isMutable Bitmap.isMutable()} returns {@code false}), and it
+ * will typically have {@code Config} {@link Bitmap.Config#HARDWARE}. Although
+ * these properties can be changed with {@link #setMutableRequired setMutableRequired(true)}
+ * (which is only compatible with {@link #decodeBitmap(Source)} and
+ * {@link #decodeBitmap(Source, OnHeaderDecodedListener)}) and {@link #setAllocator},
+ * it is also possible to apply custom effects regardless of the mutability of
+ * the final returned object by passing a {@link PostProcessor} to
+ * {@link #setPostProcessor setPostProcessor}. A {@link PostProcessor} can also be a lambda:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ * decoder.setPostProcessor((canvas) -&gt; {
+ * // This will create rounded corners.
+ * Path path = new Path();
+ * path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ * int width = canvas.getWidth();
+ * int height = canvas.getHeight();
+ * 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;
+ * });
+ * });
+ * </pre>
+ *
+ * <p>If the encoded image is incomplete or contains an error, or if an
+ * {@link Exception} occurs during decoding, a {@link DecodeException DecodeException}
+ * will be thrown. In some cases, the {@link ImageDecoder} may have decoded part of
+ * the image. In order to display the partial image, an
+ * {@link OnPartialImageListener OnPartialImageListener} must be passed to
+ * {@link #setOnPartialImageListener setOnPartialImageListener}. For example:
+ *
+ * <pre class="prettyprint">
+ * Drawable drawable = ImageDecoder.decodeDrawable(source, (decoder, info, src) -&gt; {
+ * decoder.setOnPartialImageListener((DecodeException e) -&gt; {
+ * // Returning true indicates to create a Drawable or Bitmap even
+ * // if the whole image could not be decoded. Any remaining lines
+ * // will be blank.
+ * return true;
+ * });
+ * });
+ * </pre>
*/
public final class ImageDecoder implements AutoCloseable {
+ /** @hide **/
+ public static int sApiLevel;
/**
- * Source of the encoded image data.
+ * Source of encoded image data.
+ *
+ * <p>References the data that will be used to decode a {@link Drawable}
+ * or {@link Bitmap} in {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}. Constructing a {@code Source} (with
+ * one of the overloads of {@code createSource}) can be done on any thread
+ * because the construction simply captures values. The real work is done
+ * in {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}.
+ *
+ * <p>A {@code Source} object can be reused to create multiple versions of the
+ * same image. For example, to decode a full size image and its thumbnail,
+ * the same {@code Source} can be used once with no
+ * {@link OnHeaderDecodedListener OnHeaderDecodedListener} and once with an
+ * implementation of {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}
+ * that calls {@link #setTargetSize} with smaller dimensions. One {@code Source}
+ * even used simultaneously in multiple threads.</p>
*/
public static abstract class Source {
private Source() {}
@@ -58,7 +199,7 @@ public final class ImageDecoder implements AutoCloseable {
int getDensity() { return Bitmap.DENSITY_NONE; }
/* @hide */
- int computeDstDensity() {
+ final int computeDstDensity() {
Resources res = getResources();
if (res == null) {
return Bitmap.getDefaultDensity();
@@ -84,7 +225,7 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ return nCreate(mData, mOffset, mLength, this);
}
}
@@ -96,27 +237,126 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ if (!mBuffer.isDirect() && mBuffer.hasArray()) {
+ int offset = mBuffer.arrayOffset() + mBuffer.position();
+ int length = mBuffer.limit() - mBuffer.position();
+ return nCreate(mBuffer.array(), offset, length, this);
+ }
+ ByteBuffer buffer = mBuffer.slice();
+ return nCreate(buffer, buffer.position(), buffer.limit(), this);
}
}
private static class ContentResolverSource extends Source {
- ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri) {
+ ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri,
+ @Nullable Resources res) {
mResolver = resolver;
mUri = uri;
+ mResources = res;
}
private final ContentResolver mResolver;
private final Uri mUri;
+ private final Resources mResources;
+
+ @Nullable
+ Resources getResources() { return mResources; }
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ AssetFileDescriptor assetFd = null;
+ try {
+ if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) {
+ assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
+ "image/*", null);
+ } else {
+ assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
+ }
+ } catch (FileNotFoundException e) {
+ // Some images cannot be opened as AssetFileDescriptors (e.g.
+ // bmp, ico). Open them as InputStreams.
+ InputStream is = mResolver.openInputStream(mUri);
+ if (is == null) {
+ throw new FileNotFoundException(mUri.toString());
+ }
+
+ return createFromStream(is, true, this);
+ }
+
+ final FileDescriptor fd = assetFd.getFileDescriptor();
+ final long offset = assetFd.getStartOffset();
+
+ ImageDecoder decoder = null;
+ try {
+ try {
+ Os.lseek(fd, offset, SEEK_SET);
+ decoder = nCreate(fd, this);
+ } catch (ErrnoException e) {
+ decoder = createFromStream(new FileInputStream(fd), true, this);
+ }
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(assetFd);
+ } else {
+ decoder.mAssetFd = assetFd;
+ }
+ }
+ return decoder;
+ }
+ }
+
+ @NonNull
+ private static ImageDecoder createFromFile(@NonNull File file,
+ @NonNull Source source) throws IOException {
+ FileInputStream stream = new FileInputStream(file);
+ FileDescriptor fd = stream.getFD();
+ try {
+ Os.lseek(fd, 0, SEEK_CUR);
+ } catch (ErrnoException e) {
+ return createFromStream(stream, true, source);
}
+
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(fd, source);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(stream);
+ } else {
+ decoder.mInputStream = stream;
+ decoder.mOwnsInputStream = true;
+ }
+ }
+ return decoder;
+ }
+
+ @NonNull
+ private static ImageDecoder createFromStream(@NonNull InputStream is,
+ boolean closeInputStream, Source source) throws IOException {
+ // Arbitrary size matches BitmapFactory.
+ byte[] storage = new byte[16 * 1024];
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(is, storage, source);
+ } finally {
+ if (decoder == null) {
+ if (closeInputStream) {
+ IoUtils.closeQuietly(is);
+ }
+ } else {
+ decoder.mInputStream = is;
+ decoder.mOwnsInputStream = closeInputStream;
+ decoder.mTempStorage = storage;
+ }
+ }
+
+ return decoder;
}
/**
* For backwards compatibility, this does *not* close the InputStream.
+ *
+ * Further, unlike other Sources, this one is not reusable.
*/
private static class InputStreamSource extends Source {
InputStreamSource(Resources res, InputStream is, int inputDensity) {
@@ -140,7 +380,15 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+
+ synchronized (this) {
+ if (mInputStream == null) {
+ throw new IOException("Cannot reuse InputStreamSource");
+ }
+ InputStream is = mInputStream;
+ mInputStream = null;
+ return createFromStream(is, false, this);
+ }
}
}
@@ -178,7 +426,14 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ synchronized (this) {
+ if (mAssetInputStream == null) {
+ throw new IOException("Cannot reuse AssetInputStreamSource");
+ }
+ AssetInputStream ais = mAssetInputStream;
+ mAssetInputStream = null;
+ return createFromAsset(ais, this);
+ }
}
}
@@ -192,16 +447,70 @@ public final class ImageDecoder implements AutoCloseable {
final Resources mResources;
final int mResId;
int mResDensity;
+ private Object mLock = new Object();
@Override
public Resources getResources() { return mResources; }
@Override
- public int getDensity() { return mResDensity; }
+ public int getDensity() {
+ synchronized (mLock) {
+ return mResDensity;
+ }
+ }
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ TypedValue value = new TypedValue();
+ // This is just used in order to access the underlying Asset and
+ // keep it alive.
+ InputStream is = mResources.openRawResource(mResId, value);
+
+ synchronized (mLock) {
+ if (value.density == TypedValue.DENSITY_DEFAULT) {
+ mResDensity = DisplayMetrics.DENSITY_DEFAULT;
+ } else if (value.density != TypedValue.DENSITY_NONE) {
+ mResDensity = value.density;
+ }
+ }
+
+ return createFromAsset((AssetInputStream) is, this);
+ }
+ }
+
+ /**
+ * ImageDecoder will own the AssetInputStream.
+ */
+ private static ImageDecoder createFromAsset(AssetInputStream ais,
+ Source source) throws IOException {
+ ImageDecoder decoder = null;
+ try {
+ long asset = ais.getNativeAsset();
+ decoder = nCreate(asset, source);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(ais);
+ } else {
+ decoder.mInputStream = ais;
+ decoder.mOwnsInputStream = true;
+ }
+ }
+ return decoder;
+ }
+
+ private static class AssetSource extends Source {
+ AssetSource(@NonNull AssetManager assets, @NonNull String fileName) {
+ mAssets = assets;
+ mFileName = fileName;
+ }
+
+ private final AssetManager mAssets;
+ private final String mFileName;
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ InputStream is = mAssets.open(mFileName);
+ return createFromAsset((AssetInputStream) is, this);
}
}
@@ -214,17 +523,19 @@ public final class ImageDecoder implements AutoCloseable {
@Override
public ImageDecoder createImageDecoder() throws IOException {
- return new ImageDecoder();
+ return createFromFile(mFile, this);
}
}
/**
- * Contains information about the encoded image.
+ * Information about an encoded image.
*/
public static class ImageInfo {
+ private final Size mSize;
private ImageDecoder mDecoder;
private ImageInfo(@NonNull ImageDecoder decoder) {
+ mSize = new Size(decoder.mWidth, decoder.mHeight);
mDecoder = decoder;
}
@@ -233,7 +544,7 @@ public final class ImageDecoder implements AutoCloseable {
*/
@NonNull
public Size getSize() {
- return new Size(0, 0);
+ return mSize;
}
/**
@@ -241,100 +552,271 @@ public final class ImageDecoder implements AutoCloseable {
*/
@NonNull
public String getMimeType() {
- return "";
+ return mDecoder.getMimeType();
}
/**
* Whether the image is animated.
*
- * <p>Calling {@link #decodeDrawable} will return an
- * {@link AnimatedImageDrawable}.</p>
+ * <p>If {@code true}, {@link #decodeDrawable decodeDrawable} will
+ * return an {@link AnimatedImageDrawable}.</p>
*/
public boolean isAnimated() {
return mDecoder.mAnimated;
}
+
+ /**
+ * If known, the color space the decoded bitmap will have. Note that the
+ * output color space is not guaranteed to be the color space the bitmap
+ * is encoded with. If not known (when the config is
+ * {@link Bitmap.Config#ALPHA_8} for instance), or there is an error,
+ * it is set to null.
+ */
+ @Nullable
+ public ColorSpace getColorSpace() {
+ return mDecoder.getColorSpace();
+ }
};
- /**
- * Thrown if the provided data is incomplete.
+ /** @removed
+ * @deprecated Subsumed by {@link #DecodeException}.
*/
+ @Deprecated
public static class IncompleteException extends IOException {};
/**
- * Optional listener supplied to {@link #decodeDrawable} or
- * {@link #decodeBitmap}.
+ * Interface for changing the default settings of a decode.
+ *
+ * <p>Supply an instance to
+ * {@link #decodeDrawable(Source, OnHeaderDecodedListener) decodeDrawable}
+ * or {@link #decodeBitmap(Source, OnHeaderDecodedListener) decodeBitmap},
+ * which will call {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}
+ * (in the same thread) once the size is known. The implementation of
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded} can then
+ * change the decode settings as desired.
*/
- public interface OnHeaderDecodedListener {
+ public static interface OnHeaderDecodedListener {
/**
- * Called when the header is decoded and the size is known.
+ * Called by {@link ImageDecoder} when the header has been decoded and
+ * the image size is known.
*
- * @param decoder allows changing the default settings of the decode.
- * @param info Information about the encoded image.
- * @param source that created the decoder.
+ * @param decoder the object performing the decode, for changing
+ * its default settings.
+ * @param info information about the encoded image.
+ * @param source object that created {@code decoder}.
*/
- void onHeaderDecoded(@NonNull ImageDecoder decoder,
+ public void onHeaderDecoded(@NonNull ImageDecoder decoder,
@NonNull ImageInfo info, @NonNull Source source);
};
- /**
- * An Exception was thrown reading the {@link Source}.
+ /** @removed
+ * @deprecated Replaced by {@link #DecodeException#SOURCE_EXCEPTION}.
*/
+ @Deprecated
public static final int ERROR_SOURCE_EXCEPTION = 1;
- /**
- * The encoded data was incomplete.
+ /** @removed
+ * @deprecated Replaced by {@link #DecodeException#SOURCE_INCOMPLETE}.
*/
+ @Deprecated
public static final int ERROR_SOURCE_INCOMPLETE = 2;
- /**
- * The encoded data contained an error.
+ /** @removed
+ * @deprecated Replaced by {@link #DecodeException#SOURCE_MALFORMED_DATA}.
*/
+ @Deprecated
public static final int ERROR_SOURCE_ERROR = 3;
- @Retention(SOURCE)
- public @interface Error {}
+ /**
+ * Information about an interrupted decode.
+ */
+ public static final class DecodeException extends IOException {
+ /**
+ * An Exception was thrown reading the {@link Source}.
+ */
+ public static final int SOURCE_EXCEPTION = 1;
+
+ /**
+ * The encoded data was incomplete.
+ */
+ public static final int SOURCE_INCOMPLETE = 2;
+
+ /**
+ * The encoded data contained an error.
+ */
+ public static final int SOURCE_MALFORMED_DATA = 3;
+
+ /** @hide **/
+ @Retention(SOURCE)
+ @IntDef(value = { SOURCE_EXCEPTION, SOURCE_INCOMPLETE, SOURCE_MALFORMED_DATA },
+ prefix = {"SOURCE_"})
+ public @interface Error {};
+
+ @Error final int mError;
+ @NonNull final Source mSource;
+
+ DecodeException(@Error int error, @Nullable Throwable cause, @NonNull Source source) {
+ super(errorMessage(error, cause), cause);
+ mError = error;
+ mSource = source;
+ }
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ DecodeException(@Error int error, @Nullable String msg, @Nullable Throwable cause,
+ @NonNull Source source) {
+ super(msg + errorMessage(error, cause), cause);
+ mError = error;
+ mSource = source;
+ }
+
+ /**
+ * Retrieve the reason that decoding was interrupted.
+ *
+ * <p>If the error is {@link #SOURCE_EXCEPTION}, the underlying
+ * {@link java.lang.Throwable} can be retrieved with
+ * {@link java.lang.Throwable#getCause}.</p>
+ */
+ @Error
+ public int getError() {
+ return mError;
+ }
+
+ /**
+ * Retrieve the {@link Source Source} that was interrupted.
+ *
+ * <p>This can be used for equality checking to find the Source which
+ * failed to completely decode.</p>
+ */
+ @NonNull
+ public Source getSource() {
+ return mSource;
+ }
+
+ private static String errorMessage(@Error int error, @Nullable Throwable cause) {
+ switch (error) {
+ case SOURCE_EXCEPTION:
+ return "Exception in input: " + cause;
+ case SOURCE_INCOMPLETE:
+ return "Input was incomplete.";
+ case SOURCE_MALFORMED_DATA:
+ return "Input contained an error.";
+ default:
+ return "";
+ }
+ }
+ }
/**
- * Optional listener supplied to the ImageDecoder.
+ * Interface for inspecting a {@link DecodeException DecodeException}
+ * and potentially preventing it from being thrown.
*
- * Without this listener, errors will throw {@link java.io.IOException}.
+ * <p>If an instance is passed to
+ * {@link #setOnPartialImageListener setOnPartialImageListener}, a
+ * {@link DecodeException DecodeException} that would otherwise have been
+ * thrown can be inspected inside
+ * {@link OnPartialImageListener#onPartialImage onPartialImage}.
+ * If {@link OnPartialImageListener#onPartialImage onPartialImage} returns
+ * {@code true}, a partial image will be created.
*/
- public interface OnPartialImageListener {
+ public static interface OnPartialImageListener {
/**
- * Called when there is only a partial image to display.
+ * Called by {@link ImageDecoder} when there is only a partial image to
+ * display.
*
- * If decoding is interrupted after having decoded a partial image,
- * this listener lets the client know that and allows them to
- * optionally finish the rest of the decode/creation process to create
- * a partial {@link Drawable}/{@link Bitmap}.
+ * <p>If decoding is interrupted after having decoded a partial image,
+ * this method will be called. The implementation can inspect the
+ * {@link DecodeException DecodeException} and optionally finish the
+ * rest of the decode creation process to create a partial {@link Drawable}
+ * or {@link Bitmap}.
*
- * @param error indicating what interrupted the decode.
- * @param source that had the error.
- * @return True to create and return a {@link Drawable}/{@link Bitmap}
- * with partial data. False (which is the default) to abort the
- * decode and throw {@link java.io.IOException}.
+ * @param exception exception containing information about the
+ * decode interruption.
+ * @return {@code true} to create and return a {@link Drawable} or
+ * {@link Bitmap} with partial data. {@code false} (which is the
+ * default) to abort the decode and throw {@code e}. Any undecoded
+ * lines in the image will be blank.
*/
- boolean onPartialImage(@Error int error, @NonNull Source source);
+ boolean onPartialImage(@NonNull DecodeException exception);
+ };
+
+ // Fields
+ private long mNativePtr;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mAnimated;
+ private final boolean mIsNinePatch;
+
+ private int mDesiredWidth;
+ private int mDesiredHeight;
+ private int mAllocator = ALLOCATOR_DEFAULT;
+ private boolean mUnpremultipliedRequired = false;
+ private boolean mMutable = false;
+ private boolean mConserveMemory = false;
+ private boolean mDecodeAsAlphaMask = false;
+ private ColorSpace mDesiredColorSpace = null;
+ private Rect mCropRect;
+ private Rect mOutPaddingRect;
+ private Source mSource;
+
+ private PostProcessor mPostProcessor;
+ private OnPartialImageListener mOnPartialImageListener;
+
+ // Objects for interacting with the input.
+ private InputStream mInputStream;
+ private boolean mOwnsInputStream;
+ private byte[] mTempStorage;
+ private AssetFileDescriptor mAssetFd;
+ private final AtomicBoolean mClosed = new AtomicBoolean();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ /**
+ * Private constructor called by JNI. {@link #close} must be
+ * called after decoding to delete native resources.
+ */
+ @SuppressWarnings("unused")
+ private ImageDecoder(long nativePtr, int width, int height,
+ boolean animated, boolean isNinePatch) {
+ mNativePtr = nativePtr;
+ mWidth = width;
+ mHeight = height;
+ mDesiredWidth = width;
+ mDesiredHeight = height;
+ mAnimated = animated;
+ mIsNinePatch = isNinePatch;
+ mCloseGuard.open("close");
}
- private boolean mAnimated;
- private Rect mOutPaddingRect;
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ // Avoid closing these in finalizer.
+ mInputStream = null;
+ mAssetFd = null;
- public ImageDecoder() {
- mAnimated = true; // This is too avoid throwing an exception in AnimatedImageDrawable
+ close();
+ } finally {
+ super.finalize();
+ }
}
/**
- * Create a new {@link Source} from an asset.
- * @hide
+ * Create a new {@link Source Source} from a resource.
*
* @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}.
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull Resources res, int resId)
{
@@ -342,34 +824,71 @@ public final class ImageDecoder implements AutoCloseable {
}
/**
- * Create a new {@link Source} from a {@link android.net.Uri}.
+ * Create a new {@link Source Source} from a {@link android.net.Uri}.
+ *
+ * <h5>Accepts the following URI schemes:</h5>
+ * <ul>
+ * <li>content ({@link ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link ContentResolver#SCHEME_ANDROID_RESOURCE})</li>
+ * <li>file ({@link ContentResolver#SCHEME_FILE})</li>
+ * </ul>
*
* @param cr to retrieve from.
* @param uri of the image file.
* @return a new Source object, which can be passed to
- * {@link #decodeDrawable} or {@link #decodeBitmap}.
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull ContentResolver cr,
@NonNull Uri uri) {
- return new ContentResolverSource(cr, uri);
+ return new ContentResolverSource(cr, uri, null);
+ }
+
+ /**
+ * Provide Resources for density scaling.
+ *
+ * @hide
+ */
+ @AnyThread
+ @NonNull
+ public static Source createSource(@NonNull ContentResolver cr,
+ @NonNull Uri uri, @Nullable Resources res) {
+ return new ContentResolverSource(cr, uri, res);
+ }
+
+ /**
+ * Create a new {@link Source Source} from a file in the "assets" directory.
+ */
+ @AnyThread
+ @NonNull
+ public static Source createSource(@NonNull AssetManager assets, @NonNull String fileName) {
+ return new AssetSource(assets, fileName);
}
/**
- * Create a new {@link Source} from a byte array.
+ * Create a new {@link Source 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.
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
* @throws NullPointerException if data is null.
* @throws ArrayIndexOutOfBoundsException if offset and length are
* not within data.
* @hide
*/
+ @AnyThread
@NonNull
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(
@@ -382,21 +901,29 @@ public final class ImageDecoder implements AutoCloseable {
* See {@link #createSource(byte[], int, int).
* @hide
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull byte[] data) {
return createSource(data, 0, data.length);
}
/**
- * Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
+ * Create a new {@link Source Source} from a {@link java.nio.ByteBuffer}.
+ *
+ * <p>Decoding will start from {@link java.nio.ByteBuffer#position() buffer.position()}.
+ * The position of {@code buffer} will not be affected.</p>
*
- * <p>The returned {@link Source} effectively takes ownership of the
- * {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
- * this call.</p>
+ * <p>Note: If this {@code Source} is passed to {@link #decodeDrawable decodeDrawable},
+ * and the encoded image is animated, the returned {@link AnimatedImageDrawable}
+ * will continue reading from the {@code buffer}, so its contents must not
+ * be modified, even after the {@code AnimatedImageDrawable} is returned.
+ * {@code buffer}'s contents should never be modified during decode.</p>
*
- * Decoding will start from {@link java.nio.ByteBuffer#position()}. The
- * position after decoding is undefined.
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull ByteBuffer buffer) {
return new ByteBufferSource(buffer);
@@ -404,23 +931,39 @@ public final class ImageDecoder implements AutoCloseable {
/**
* Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ *
+ * <p>Unlike other Sources, this one cannot be reused.</p>
+ *
* @hide
*/
+ @AnyThread
+ @NonNull
public static Source createSource(Resources res, InputStream is) {
return new InputStreamSource(res, is, Bitmap.getDefaultDensity());
}
/**
* Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ *
+ * <p>Unlike other Sources, this one cannot be reused.</p>
+ *
* @hide
*/
+ @AnyThread
+ @TestApi
+ @NonNull
public static Source createSource(Resources res, InputStream is, int density) {
return new InputStreamSource(res, is, density);
}
/**
- * Create a new {@link Source} from a {@link java.io.File}.
+ * Create a new {@link Source Source} from a {@link java.io.File}.
+ *
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable decodeDrawable} or
+ * {@link #decodeBitmap decodeBitmap}.
*/
+ @AnyThread
@NonNull
public static Source createSource(@NonNull File file) {
return new FileSource(file);
@@ -431,39 +974,142 @@ public final class ImageDecoder implements AutoCloseable {
*
* <p>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
+ * height that can be achieved 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 #setRequireUnpremultiplied} set to {@code true}.</p>
+ * {@link #setUnpremultipliedRequired} set to {@code true}.</p>
*
* @param sampleSize Sampling rate of the encoded image.
* @return {@link android.util.Size} of the width and height after
* sampling.
+ *
+ * @hide
*/
@NonNull
public Size getSampledSize(int sampleSize) {
- return new Size(0, 0);
+ if (sampleSize <= 0) {
+ throw new IllegalArgumentException("sampleSize must be positive! "
+ + "provided " + sampleSize);
+ }
+ if (mNativePtr == 0) {
+ throw new IllegalStateException("ImageDecoder is closed!");
+ }
+
+ return nGetSampledSize(mNativePtr, sampleSize);
}
// Modifiers
+ /** @removed
+ * @deprecated Renamed to {@link #setTargetSize}.
+ */
+ @Deprecated
+ public ImageDecoder setResize(int width, int height) {
+ this.setTargetSize(width, height);
+ return this;
+ }
+
/**
- * Resize the output to have the following size.
+ * Specify the size of the output {@link Drawable} or {@link Bitmap}.
+ *
+ * <p>By default, the output size will match the size of the encoded
+ * image, which can be retrieved from the {@link ImageInfo ImageInfo} in
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
+ * <p>This will sample or scale the output to an arbitrary size that may
+ * be smaller or larger than the encoded size.</p>
+ *
+ * <p>Only the last call to this or {@link #setTargetSampleSize} is
+ * respected.</p>
*
- * @param width must be greater than 0.
- * @param height must be greater than 0.
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
+ * @param width width in pixels of the output, must be greater than 0
+ * @param height height in pixels of the output, must be greater than 0
+ */
+ public void setTargetSize(@Px @IntRange(from = 1) int width,
+ @Px @IntRange(from = 1) int height) {
+ if (width <= 0 || height <= 0) {
+ throw new IllegalArgumentException("Dimensions must be positive! "
+ + "provided (" + width + ", " + height + ")");
+ }
+
+ mDesiredWidth = width;
+ mDesiredHeight = height;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setTargetSampleSize}.
*/
- public void setResize(int width, int height) {
+ @Deprecated
+ public ImageDecoder setResize(int sampleSize) {
+ this.setTargetSampleSize(sampleSize);
+ return this;
+ }
+
+ private int getTargetDimension(int original, int sampleSize, int computed) {
+ // Sampling will never result in a smaller size than 1.
+ if (sampleSize >= original) {
+ return 1;
+ }
+
+ // Use integer divide to find the desired size. If that is what
+ // getSampledSize computed, that is the size to use.
+ int target = original / sampleSize;
+ if (computed == target) {
+ return computed;
+ }
+
+ // If sampleSize does not divide evenly into original, the decoder
+ // may round in either direction. It just needs to get a result that
+ // is close.
+ int reverse = computed * sampleSize;
+ if (Math.abs(reverse - original) < sampleSize) {
+ // This is the size that can be decoded most efficiently.
+ return computed;
+ }
+
+ // The decoder could not get close (e.g. it is a DNG image).
+ return target;
}
/**
- * Resize based on a sample size.
+ * Set the target size with a sampleSize.
*
- * <p>This has the same effect as passing the result of
- * {@link #getSampledSize} to {@link #setResize(int, int)}.</p>
+ * <p>By default, the output size will match the size of the encoded
+ * image, which can be retrieved from the {@link ImageInfo ImageInfo} in
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
*
- * @param sampleSize Sampling rate of the encoded image.
+ * <p>Requests the decoder to subsample the original image, returning a
+ * smaller image to save memory. The {@code sampleSize} is the number of pixels
+ * in either dimension that correspond to a single pixel in the output.
+ * For example, {@code sampleSize == 4} returns an image that is 1/4 the
+ * width/height of the original, and 1/16 the number of pixels.</p>
+ *
+ * <p>Must be greater than or equal to 1.</p>
+ *
+ * <p>This has the same effect as calling {@link #setTargetSize} with
+ * dimensions based on the {@code sampleSize}. Unlike dividing the original
+ * width and height by the {@code sampleSize} manually, calling this method
+ * allows {@code ImageDecoder} to round in the direction that it can do most
+ * efficiently.</p>
+ *
+ * <p>Only the last call to this or {@link #setTargetSize} is respected.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
+ * @param sampleSize sampling rate of the encoded image.
*/
- public void setResize(int sampleSize) {
+ public void setTargetSampleSize(@IntRange(from = 1) int sampleSize) {
+ Size size = this.getSampledSize(sampleSize);
+ int targetWidth = getTargetDimension(mWidth, sampleSize, size.getWidth());
+ int targetHeight = getTargetDimension(mHeight, sampleSize, size.getHeight());
+ this.setTargetSize(targetWidth, targetHeight);
+ }
+
+ private boolean requestedResize() {
+ return mWidth != mDesiredWidth || mHeight != mDesiredHeight;
}
// These need to stay in sync with ImageDecoder.cpp's Allocator enum.
@@ -473,14 +1119,15 @@ public final class ImageDecoder implements AutoCloseable {
* 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}.
+ * {@link #setMutableRequired setMutableRequired(true)} or
+ * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)}.
*/
public static final int ALLOCATOR_DEFAULT = 0;
/**
* Use a software allocation for the pixel memory.
*
- * Useful for drawing to a software {@link Canvas} or for
+ * <p>Useful for drawing to a software {@link Canvas} or for
* accessing the pixels on the final output.
*/
public static final int ALLOCATOR_SOFTWARE = 1;
@@ -488,92 +1135,177 @@ public final class ImageDecoder implements AutoCloseable {
/**
* Use shared memory for the pixel memory.
*
- * Useful for sharing across processes.
+ * <p>Useful for sharing across processes.
*/
public static final int ALLOCATOR_SHARED_MEMORY = 2;
/**
* Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}.
*
- * When this is combined with incompatible options, like
- * {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable}
- * / {@link #decodeBitmap} will throw an
- * {@link java.lang.IllegalStateException}.
+ * <p>When this is combined with incompatible options, like
+ * {@link #setMutableRequired setMutableRequired(true)} or
+ * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)},
+ * {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap decodeBitmap}
+ * will throw an {@link java.lang.IllegalStateException}.
*/
public static final int ALLOCATOR_HARDWARE = 3;
/** @hide **/
@Retention(SOURCE)
+ @IntDef(value = { ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE,
+ ALLOCATOR_SHARED_MEMORY, ALLOCATOR_HARDWARE },
+ prefix = {"ALLOCATOR_"})
public @interface Allocator {};
/**
* Choose the backing for the pixel memory.
*
- * This is ignored for animated drawables.
+ * <p>This is ignored for animated drawables.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
*
* @param allocator Type of allocator to use.
*/
- public ImageDecoder setAllocator(@Allocator int allocator) {
- return this;
+ public void setAllocator(@Allocator int allocator) {
+ if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) {
+ throw new IllegalArgumentException("invalid allocator " + allocator);
+ }
+ mAllocator = allocator;
+ }
+
+ /**
+ * Return the allocator for the pixel memory.
+ */
+ @Allocator
+ public int getAllocator() {
+ return mAllocator;
}
/**
* Specify whether the {@link Bitmap} should have unpremultiplied pixels.
*
- * By default, ImageDecoder will create a {@link Bitmap} with
+ * <p>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 with a value of {@code true} will result in
* {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied
- * pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with
- * {@link #decodeDrawable}; attempting to decode an unpremultiplied
- * {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
+ * pixels. See {@link Bitmap#isPremultiplied Bitmap.isPremultiplied()}.
+ * This is incompatible with {@link #decodeDrawable decodeDrawable};
+ * attempting to decode an unpremultiplied {@link Drawable} will throw an
+ * {@link java.lang.IllegalStateException}. </p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setUnpremultipliedRequired(boolean unpremultipliedRequired) {
+ mUnpremultipliedRequired = unpremultipliedRequired;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setUnpremultipliedRequired}.
*/
- public ImageDecoder setRequireUnpremultiplied(boolean requireUnpremultiplied) {
+ @Deprecated
+ public ImageDecoder setRequireUnpremultiplied(boolean unpremultipliedRequired) {
+ this.setUnpremultipliedRequired(unpremultipliedRequired);
return this;
}
/**
+ * Return whether the {@link Bitmap} will have unpremultiplied pixels.
+ */
+ public boolean isUnpremultipliedRequired() {
+ return mUnpremultipliedRequired;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isUnpremultipliedRequired}.
+ */
+ @Deprecated
+ public boolean getRequireUnpremultiplied() {
+ return this.isUnpremultipliedRequired();
+ }
+
+ /**
* Modify the image after decoding and scaling.
*
* <p>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.</p>
*
+ * <p>If combined with {@link #setTargetSize} and/or {@link #setCrop},
+ * {@link PostProcessor#onPostProcess} occurs last.</p>
+ *
* <p>If set on a nine-patch image, the nine-patch data is ignored.</p>
*
* <p>For an animated image, the drawing commands drawn on the
* {@link Canvas} will be recorded immediately and then applied to each
* frame.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
*/
- public ImageDecoder setPostProcessor(@Nullable PostProcessor p) {
- return this;
+ public void setPostProcessor(@Nullable PostProcessor postProcessor) {
+ mPostProcessor = postProcessor;
+ }
+
+ /**
+ * Return the {@link PostProcessor} currently set.
+ */
+ @Nullable
+ public PostProcessor getPostProcessor() {
+ return mPostProcessor;
}
/**
* Set (replace) the {@link OnPartialImageListener} on this object.
*
- * Will be called if there is an error in the input. Without one, a
- * partial {@link Bitmap} will be created.
+ * <p>Will be called if there is an error in the input. Without one, an
+ * error will result in an {@code Exception} being thrown.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
*/
- public ImageDecoder setOnPartialImageListener(@Nullable OnPartialImageListener l) {
- return this;
+ public void setOnPartialImageListener(@Nullable OnPartialImageListener listener) {
+ mOnPartialImageListener = listener;
+ }
+
+ /**
+ * Return the {@link OnPartialImageListener OnPartialImageListener} currently set.
+ */
+ @Nullable
+ public OnPartialImageListener getOnPartialImageListener() {
+ return mOnPartialImageListener;
}
/**
* Crop the output to {@code subset} of the (possibly) scaled image.
*
* <p>{@code subset} must be contained within the size set by
- * {@link #setResize} or the bounds of the image if setResize was not
- * called. Otherwise an {@link IllegalStateException} will be thrown by
- * {@link #decodeDrawable}/{@link #decodeBitmap}.</p>
+ * {@link #setTargetSize} or the bounds of the image if setTargetSize was
+ * not called. Otherwise an {@link IllegalStateException} will be thrown by
+ * {@link #decodeDrawable decodeDrawable}/{@link #decodeBitmap decodeBitmap}.</p>
*
* <p>NOT intended as a replacement for
- * {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
- * but merely crops the output.</p>
+ * {@link BitmapRegionDecoder#decodeRegion BitmapRegionDecoder.decodeRegion()}.
+ * This supports all formats, but merely crops the output.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
*/
- public ImageDecoder setCrop(@Nullable Rect subset) {
- return this;
+ public void setCrop(@Nullable Rect subset) {
+ mCropRect = subset;
+ }
+
+ /**
+ * Return the cropping rectangle, if set.
+ */
+ @Nullable
+ public Rect getCrop() {
+ return mCropRect;
}
/**
@@ -582,43 +1314,122 @@ public final class ImageDecoder implements AutoCloseable {
* If the image is a nine patch, this Rect will be set to the padding
* rectangle during decode. Otherwise it will not be modified.
*
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ *
* @hide
*/
- public ImageDecoder setOutPaddingRect(@NonNull Rect outPadding) {
+ public void setOutPaddingRect(@NonNull Rect outPadding) {
mOutPaddingRect = outPadding;
- return this;
}
/**
* Specify whether the {@link Bitmap} should be mutable.
*
- * <p>By default, a {@link Bitmap} created will be immutable, but that can
- * be changed with this call.</p>
+ * <p>By default, a {@link Bitmap} created by {@link #decodeBitmap decodeBitmap}
+ * will be immutable i.e. {@link Bitmap#isMutable() Bitmap.isMutable()} returns
+ * {@code false}. This can be changed with {@code setMutableRequired(true)}.
*
* <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE},
* because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable.
* Attempting to combine them will throw an
* {@link java.lang.IllegalStateException}.</p>
*
- * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable},
+ * <p>Mutable Bitmaps are also incompatible with {@link #decodeDrawable 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}.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setMutableRequired(boolean mutable) {
+ mMutable = mutable;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setMutableRequired}.
*/
+ @Deprecated
public ImageDecoder setMutable(boolean mutable) {
+ this.setMutableRequired(mutable);
return this;
}
/**
- * Specify whether to potentially save RAM at the expense of quality.
+ * Return whether the decoded {@link Bitmap} will be mutable.
+ */
+ public boolean isMutableRequired() {
+ return mMutable;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isMutableRequired}.
+ */
+ @Deprecated
+ public boolean getMutable() {
+ return this.isMutableRequired();
+ }
+
+ /**
+ * Save memory if possible by using a denser {@link Bitmap.Config} at the
+ * cost of some image quality.
*
- * Setting this to {@code true} 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.
+ * <p>For example an opaque 8-bit image may be compressed into an
+ * {@link Bitmap.Config#RGB_565} configuration, sacrificing image
+ * quality to save memory.
*/
- public ImageDecoder setPreferRamOverQuality(boolean preferRamOverQuality) {
- return this;
+ public static final int MEMORY_POLICY_LOW_RAM = 0;
+
+ /**
+ * Use the most natural {@link Bitmap.Config} for the internal {@link Bitmap}.
+ *
+ * <p>This is the recommended default for most applications and usages. This
+ * will use the closest {@link Bitmap.Config} for the encoded source. If the
+ * encoded source does not exactly match any {@link Bitmap.Config}, the next
+ * highest quality {@link Bitmap.Config} will be used avoiding any loss in
+ * image quality.
+ */
+ public static final int MEMORY_POLICY_DEFAULT = 1;
+
+ /** @hide **/
+ @Retention(SOURCE)
+ @IntDef(value = { MEMORY_POLICY_DEFAULT, MEMORY_POLICY_LOW_RAM },
+ prefix = {"MEMORY_POLICY_"})
+ public @interface MemoryPolicy {};
+
+ /**
+ * Specify the memory policy for the decoded {@link Bitmap}.
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setMemorySizePolicy(@MemoryPolicy int policy) {
+ mConserveMemory = (policy == MEMORY_POLICY_LOW_RAM);
+ }
+
+ /**
+ * Retrieve the memory policy for the decoded {@link Bitmap}.
+ */
+ @MemoryPolicy
+ public int getMemorySizePolicy() {
+ return mConserveMemory ? MEMORY_POLICY_LOW_RAM : MEMORY_POLICY_DEFAULT;
+ }
+
+ /** @removed
+ * @deprecated Replaced by {@link #setMemorySizePolicy}.
+ */
+ @Deprecated
+ public void setConserveMemory(boolean conserveMemory) {
+ mConserveMemory = conserveMemory;
+ }
+
+ /** @removed
+ * @deprecated Replaced by {@link #getMemorySizePolicy}.
+ */
+ @Deprecated
+ public boolean getConserveMemory() {
+ return mConserveMemory;
}
/**
@@ -628,77 +1439,467 @@ public final class ImageDecoder implements AutoCloseable {
* with only one channel, treat that channel as alpha. Otherwise this call has
* no effect.</p>
*
- * <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
- * combine them will result in {@link #decodeDrawable}/
- * {@link #decodeBitmap} throwing an
+ * <p>This is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
+ * combine them will result in {@link #decodeDrawable decodeDrawable}/
+ * {@link #decodeBitmap decodeBitmap} throwing an
* {@link java.lang.IllegalStateException}.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setDecodeAsAlphaMaskEnabled(boolean enabled) {
+ mDecodeAsAlphaMask = enabled;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}.
+ */
+ @Deprecated
+ public ImageDecoder setDecodeAsAlphaMask(boolean enabled) {
+ this.setDecodeAsAlphaMaskEnabled(enabled);
+ return this;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #setDecodeAsAlphaMaskEnabled}.
*/
+ @Deprecated
public ImageDecoder setAsAlphaMask(boolean asAlphaMask) {
+ this.setDecodeAsAlphaMask(asAlphaMask);
return this;
}
+ /**
+ * Return whether to treat single channel input as alpha.
+ *
+ * <p>This returns whether {@link #setDecodeAsAlphaMaskEnabled} was set to
+ * {@code true}. It may still return {@code true} even if the image has
+ * more than one channel and therefore will not be treated as an alpha
+ * mask.</p>
+ */
+ public boolean isDecodeAsAlphaMaskEnabled() {
+ return mDecodeAsAlphaMask;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}.
+ */
+ @Deprecated
+ public boolean getDecodeAsAlphaMask() {
+ return mDecodeAsAlphaMask;
+ }
+
+ /** @removed
+ * @deprecated Renamed to {@link #isDecodeAsAlphaMaskEnabled}.
+ */
+ @Deprecated
+ public boolean getAsAlphaMask() {
+ return this.getDecodeAsAlphaMask();
+ }
+
+ /**
+ * Specify the desired {@link ColorSpace} for the output.
+ *
+ * <p>If non-null, the decoder will try to decode into {@code colorSpace}.
+ * If it is null, which is the default, or the request cannot be met, the
+ * decoder will pick either the color space embedded in the image or the
+ * {@link ColorSpace} best suited for the requested image configuration
+ * (for instance {@link ColorSpace.Named#SRGB sRGB} for the
+ * {@link Bitmap.Config#ARGB_8888} configuration).</p>
+ *
+ * <p>{@link Bitmap.Config#RGBA_F16} always uses the
+ * {@link ColorSpace.Named#LINEAR_EXTENDED_SRGB scRGB} color space.
+ * Bitmaps in other configurations without an embedded color space are
+ * assumed to be in the {@link ColorSpace.Named#SRGB sRGB} color space.</p>
+ *
+ * <p class="note">Only {@link ColorSpace.Model#RGB} color spaces are
+ * currently supported. An <code>IllegalArgumentException</code> will
+ * be thrown by {@link #decodeDrawable decodeDrawable}/
+ * {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space
+ * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p>
+ *
+ * <p class="note">The specified color space's transfer function must be
+ * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An
+ * <code>IllegalArgumentException</code> will be thrown by the decode methods
+ * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the
+ * specified color space returns null.</p>
+ *
+ * <p>Like all setters on ImageDecoder, this must be called inside
+ * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p>
+ */
+ public void setTargetColorSpace(ColorSpace colorSpace) {
+ mDesiredColorSpace = colorSpace;
+ }
+
+ /**
+ * Closes this resource, relinquishing any underlying resources. This method
+ * is invoked automatically on objects managed by the try-with-resources
+ * statement.
+ *
+ * <p>This is an implementation detail of {@link ImageDecoder}, and should
+ * never be called manually.</p>
+ */
@Override
public void close() {
+ mCloseGuard.close();
+ if (!mClosed.compareAndSet(false, true)) {
+ return;
+ }
+ nClose(mNativePtr);
+ mNativePtr = 0;
+
+ if (mOwnsInputStream) {
+ IoUtils.closeQuietly(mInputStream);
+ }
+ IoUtils.closeQuietly(mAssetFd);
+
+ mInputStream = null;
+ mAssetFd = null;
+ mTempStorage = null;
+ }
+
+ private void checkState() {
+ if (mNativePtr == 0) {
+ throw new IllegalStateException("Cannot use closed ImageDecoder!");
+ }
+
+ checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
+
+ if (mAllocator == ALLOCATOR_HARDWARE) {
+ if (mMutable) {
+ throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
+ }
+ if (mDecodeAsAlphaMask) {
+ throw new IllegalStateException("Cannot make HARDWARE Alpha mask Bitmap!");
+ }
+ }
+
+ if (mPostProcessor != null && mUnpremultipliedRequired) {
+ throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
+ }
+
+ if (mDesiredColorSpace != null) {
+ if (!(mDesiredColorSpace instanceof ColorSpace.Rgb)) {
+ throw new IllegalArgumentException("The target color space must use the "
+ + "RGB color model - provided: " + mDesiredColorSpace);
+ }
+ if (((ColorSpace.Rgb) mDesiredColorSpace).getTransferParameters() == null) {
+ throw new IllegalArgumentException("The target color space must use an "
+ + "ICC parametric transfer function - provided: " + mDesiredColorSpace);
+ }
+ }
+ }
+
+ 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 + ")");
+ }
+ }
+
+ @WorkerThread
+ @NonNull
+ private Bitmap decodeBitmapInternal() throws IOException {
+ checkState();
+ return nDecodeBitmap(mNativePtr, this, mPostProcessor != null,
+ mDesiredWidth, mDesiredHeight, mCropRect,
+ mMutable, mAllocator, mUnpremultipliedRequired,
+ mConserveMemory, mDecodeAsAlphaMask, mDesiredColorSpace);
+ }
+
+ private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener,
+ @NonNull Source src) {
+ if (listener != null) {
+ ImageInfo info = new ImageInfo(this);
+ try {
+ listener.onHeaderDecoded(this, info, src);
+ } finally {
+ info.mDecoder = null;
+ }
+ }
}
/**
* Create a {@link Drawable} from a {@code Source}.
*
* @param src representing the encoded image.
- * @param listener for learning the {@link ImageInfo} and changing any
- * default settings on the {@code ImageDecoder}. If not {@code null},
- * this will be called on the same thread as {@code decodeDrawable}
- * before that method returns.
+ * @param listener for learning the {@link ImageInfo ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. This will be called on
+ * the same thread as {@code decodeDrawable} before that method returns.
+ * This is required in order to change any of the default settings.
* @return Drawable for displaying the image.
* @throws IOException if {@code src} is not found, is an unsupported
* format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Drawable decodeDrawable(@NonNull Source src,
+ @NonNull OnHeaderDecodedListener listener) throws IOException {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener cannot be null! "
+ + "Use decodeDrawable(Source) to not have a listener");
+ }
+ return decodeDrawableImpl(src, listener);
+ }
+
+ @WorkerThread
+ @NonNull
+ private static Drawable decodeDrawableImpl(@NonNull Source src,
@Nullable OnHeaderDecodedListener listener) throws IOException {
- Bitmap bitmap = decodeBitmap(src, listener);
- return new BitmapDrawable(src.getResources(), bitmap);
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ if (decoder.mUnpremultipliedRequired) {
+ // 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!");
+ }
+
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap and after decode set the density on the resulting bitmap
+ final int srcDensity = decoder.computeDensity(src);
+ if (decoder.mAnimated) {
+ // AnimatedImageDrawable calls postProcessAndRelease only if
+ // mPostProcessor exists.
+ ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
+ null : decoder;
+ Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+ postProcessPtr, decoder.mDesiredWidth,
+ decoder.mDesiredHeight, srcDensity,
+ src.computeDstDensity(), decoder.mCropRect,
+ decoder.mInputStream, decoder.mAssetFd);
+ // d has taken ownership of these objects.
+ decoder.mInputStream = null;
+ decoder.mAssetFd = null;
+ return d;
+ }
+
+ Bitmap bm = decoder.decodeBitmapInternal();
+ bm.setDensity(srcDensity);
+
+ Resources res = src.getResources();
+ byte[] np = bm.getNinePatchChunk();
+ if (np != null && NinePatch.isNinePatchChunk(np)) {
+ Rect opticalInsets = new Rect();
+ bm.getOpticalInsets(opticalInsets);
+ Rect padding = decoder.mOutPaddingRect;
+ if (padding == null) {
+ padding = new Rect();
+ }
+ nGetPadding(decoder.mNativePtr, padding);
+ return new NinePatchDrawable(res, bm, np, padding,
+ opticalInsets, null);
+ }
+
+ return new BitmapDrawable(res, bm);
+ }
}
/**
- * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}.
+ * Create a {@link Drawable} from a {@code Source}.
+ *
+ * <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener},
+ * the default settings will be used. In order to change any settings, call
+ * {@link #decodeDrawable(Source, OnHeaderDecodedListener)} instead.</p>
+ *
+ * @param src representing the encoded image.
+ * @return Drawable for displaying the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Drawable decodeDrawable(@NonNull Source src)
throws IOException {
- return decodeDrawable(src, null);
+ return decodeDrawableImpl(src, null);
}
/**
* Create a {@link Bitmap} from a {@code Source}.
*
* @param src representing the encoded image.
- * @param listener for learning the {@link ImageInfo} and changing any
- * default settings on the {@code ImageDecoder}. If not {@code null},
- * this will be called on the same thread as {@code decodeBitmap}
- * before that method returns.
+ * @param listener for learning the {@link ImageInfo ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. This will be called on
+ * the same thread as {@code decodeBitmap} before that method returns.
+ * This is required in order to change any of the default settings.
* @return Bitmap containing the image.
* @throws IOException if {@code src} is not found, is an unsupported
* format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Bitmap decodeBitmap(@NonNull Source src,
+ @NonNull OnHeaderDecodedListener listener) throws IOException {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener cannot be null! "
+ + "Use decodeBitmap(Source) to not have a listener");
+ }
+ return decodeBitmapImpl(src, listener);
+ }
+
+ @WorkerThread
+ @NonNull
+ private static Bitmap decodeBitmapImpl(@NonNull Source src,
@Nullable OnHeaderDecodedListener listener) throws IOException {
- TypedValue value = new TypedValue();
- value.density = src.getDensity();
- ImageDecoder decoder = src.createImageDecoder();
- if (listener != null) {
- listener.onHeaderDecoded(decoder, new ImageInfo(decoder), src);
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap
+ final int srcDensity = decoder.computeDensity(src);
+ Bitmap bm = decoder.decodeBitmapInternal();
+ bm.setDensity(srcDensity);
+
+ Rect padding = decoder.mOutPaddingRect;
+ if (padding != null) {
+ byte[] np = bm.getNinePatchChunk();
+ if (np != null && NinePatch.isNinePatchChunk(np)) {
+ nGetPadding(decoder.mNativePtr, padding);
+ }
+ }
+
+ return bm;
+ }
+ }
+
+ // This method may modify the decoder so it must be called prior to performing the decode
+ private int computeDensity(@NonNull Source src) {
+ // if the caller changed the size then we treat the density as unknown
+ if (this.requestedResize()) {
+ return Bitmap.DENSITY_NONE;
+ }
+
+ final int srcDensity = src.getDensity();
+ if (srcDensity == Bitmap.DENSITY_NONE) {
+ return srcDensity;
+ }
+
+ // Scaling up nine-patch divs is imprecise and is better handled
+ // at draw time. An app won't be relying on the internal Bitmap's
+ // size, so it is safe to let NinePatchDrawable handle scaling.
+ // mPostProcessor disables nine-patching, so behave normally if
+ // it is present.
+ if (mIsNinePatch && mPostProcessor == null) {
+ return srcDensity;
+ }
+
+ // Special stuff for compatibility mode: if the target density is not
+ // the same as the display density, but the resource -is- the same as
+ // the display density, then don't scale it down to the target density.
+ // This allows us to load the system's density-correct resources into
+ // an application in compatibility mode, without scaling those down
+ // to the compatibility density only to have them scaled back up when
+ // drawn to the screen.
+ Resources res = src.getResources();
+ if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) {
+ return srcDensity;
+ }
+
+ final int dstDensity = src.computeDstDensity();
+ if (srcDensity == dstDensity) {
+ return srcDensity;
}
- return BitmapFactory.decodeResourceStream(src.getResources(), value,
- ((InputStreamSource) src).mInputStream, decoder.mOutPaddingRect, null);
+
+ // For P and above, only resize if it would be a downscale. Scale up prior
+ // to P in case the app relies on the Bitmap's size without considering density.
+ if (srcDensity < dstDensity && sApiLevel >= Build.VERSION_CODES.P) {
+ return srcDensity;
+ }
+
+ float scale = (float) dstDensity / srcDensity;
+ int scaledWidth = (int) (mWidth * scale + 0.5f);
+ int scaledHeight = (int) (mHeight * scale + 0.5f);
+ this.setTargetSize(scaledWidth, scaledHeight);
+ return dstDensity;
+ }
+
+ @NonNull
+ private String getMimeType() {
+ return nGetMimeType(mNativePtr);
+ }
+
+ @Nullable
+ private ColorSpace getColorSpace() {
+ return nGetColorSpace(mNativePtr);
}
/**
- * See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}.
+ * Create a {@link Bitmap} from a {@code Source}.
+ *
+ * <p>Since there is no {@link OnHeaderDecodedListener OnHeaderDecodedListener},
+ * the default settings will be used. In order to change any settings, call
+ * {@link #decodeBitmap(Source, OnHeaderDecodedListener)} instead.</p>
+ *
+ * @param src representing the encoded image.
+ * @return Bitmap containing the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
*/
+ @WorkerThread
@NonNull
public static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
- return decodeBitmap(src, null);
+ return decodeBitmapImpl(src, null);
}
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private int postProcessAndRelease(@NonNull Canvas canvas) {
+ try {
+ return mPostProcessor.onPostProcess(canvas);
+ } finally {
+ canvas.release();
+ }
+ }
+
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private void onPartialImage(@DecodeException.Error int error, @Nullable Throwable cause)
+ throws DecodeException {
+ DecodeException exception = new DecodeException(error, cause, mSource);
+ if (mOnPartialImageListener == null
+ || !mOnPartialImageListener.onPartialImage(exception)) {
+ throw exception;
+ }
+ }
+
+ private static native ImageDecoder nCreate(long asset, Source src) throws IOException;
+ private static native ImageDecoder nCreate(ByteBuffer buffer, int position,
+ int limit, Source src) throws IOException;
+ private static native ImageDecoder nCreate(byte[] data, int offset, int length,
+ Source src) throws IOException;
+ private static native ImageDecoder nCreate(InputStream is, byte[] storage,
+ Source src) throws IOException;
+ // The fd must be seekable.
+ private static native ImageDecoder nCreate(FileDescriptor fd, Source src) throws IOException;
+ @NonNull
+ private static native Bitmap nDecodeBitmap(long nativePtr,
+ @NonNull ImageDecoder decoder,
+ boolean doPostProcess,
+ int width, int height,
+ @Nullable Rect cropRect, boolean mutable,
+ int allocator, boolean unpremulRequired,
+ boolean conserveMemory, boolean decodeAsAlphaMask,
+ @Nullable ColorSpace desiredColorSpace)
+ throws IOException;
+ private static native Size nGetSampledSize(long nativePtr,
+ int sampleSize);
+ private static native void nGetPadding(long nativePtr, @NonNull Rect outRect);
+ private static native void nClose(long nativePtr);
+ private static native String nGetMimeType(long nativePtr);
+ private static native ColorSpace nGetColorSpace(long nativePtr);
}