diff options
Diffstat (limited to 'android/graphics/ImageDecoder.java')
-rw-r--r-- | android/graphics/ImageDecoder.java | 1501 |
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) -> { + * 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) -> { + * decoder.setPostProcessor((canvas) -> { + * // 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) -> { + * decoder.setOnPartialImageListener((DecodeException e) -> { + * // 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); } |