Java tutorial
/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.graphics; import 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.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 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.annotation.Retention; import java.nio.ByteBuffer; import java.util.concurrent.atomic.AtomicBoolean; /** * <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 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() { } /* @hide */ @Nullable Resources getResources() { return null; } /* @hide */ int getDensity() { return Bitmap.DENSITY_NONE; } /* @hide */ final int computeDstDensity() { Resources res = getResources(); if (res == null) { return Bitmap.getDefaultDensity(); } return res.getDisplayMetrics().densityDpi; } /* @hide */ @NonNull abstract ImageDecoder createImageDecoder() throws IOException; }; private static class ByteArraySource extends Source { ByteArraySource(@NonNull byte[] data, int offset, int length) { mData = data; mOffset = offset; mLength = length; }; private final byte[] mData; private final int mOffset; private final int mLength; @Override public ImageDecoder createImageDecoder() throws IOException { return nCreate(mData, mOffset, mLength, this); } } private static class ByteBufferSource extends Source { ByteBufferSource(@NonNull ByteBuffer buffer) { mBuffer = buffer; } private final ByteBuffer mBuffer; @Override public ImageDecoder createImageDecoder() throws IOException { 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, @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 { 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) { if (is == null) { throw new IllegalArgumentException("The InputStream cannot be null"); } mResources = res; mInputStream = is; mInputDensity = res != null ? inputDensity : Bitmap.DENSITY_NONE; } final Resources mResources; InputStream mInputStream; final int mInputDensity; @Override public Resources getResources() { return mResources; } @Override public int getDensity() { return mInputDensity; } @Override public ImageDecoder createImageDecoder() throws IOException { synchronized (this) { if (mInputStream == null) { throw new IOException("Cannot reuse InputStreamSource"); } InputStream is = mInputStream; mInputStream = null; return createFromStream(is, false, this); } } } /** * Takes ownership of the AssetInputStream. * * @hide */ public static class AssetInputStreamSource extends Source { public AssetInputStreamSource(@NonNull AssetInputStream ais, @NonNull Resources res, @NonNull TypedValue value) { mAssetInputStream = ais; mResources = res; if (value.density == TypedValue.DENSITY_DEFAULT) { mDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (value.density != TypedValue.DENSITY_NONE) { mDensity = value.density; } else { mDensity = Bitmap.DENSITY_NONE; } } private AssetInputStream mAssetInputStream; private final Resources mResources; private final int mDensity; @Override public Resources getResources() { return mResources; } @Override public int getDensity() { return mDensity; } @Override public ImageDecoder createImageDecoder() throws IOException { synchronized (this) { if (mAssetInputStream == null) { throw new IOException("Cannot reuse AssetInputStreamSource"); } AssetInputStream ais = mAssetInputStream; mAssetInputStream = null; return createFromAsset(ais, this); } } } private static class ResourceSource extends Source { ResourceSource(@NonNull Resources res, int resId) { mResources = res; mResId = resId; mResDensity = Bitmap.DENSITY_NONE; } final Resources mResources; final int mResId; int mResDensity; private Object mLock = new Object(); @Override public Resources getResources() { return mResources; } @Override 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 { InputStream is = mAssets.open(mFileName); return createFromAsset((AssetInputStream) is, this); } } private static class FileSource extends Source { FileSource(@NonNull File file) { mFile = file; } private final File mFile; @Override public ImageDecoder createImageDecoder() throws IOException { return createFromFile(mFile, this); } } /** * 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; } /** * Size of the image, without scaling or cropping. */ @NonNull public Size getSize() { return mSize; } /** * The mimeType of the image. */ @NonNull public String getMimeType() { return mDecoder.getMimeType(); } /** * Whether the image is animated. * * <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(); } }; /** @removed * @deprecated Subsumed by {@link #DecodeException}. */ @Deprecated public static class IncompleteException extends IOException { }; /** * 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 static interface OnHeaderDecodedListener { /** * Called by {@link ImageDecoder} when the header has been decoded and * the image size is known. * * @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}. */ public void onHeaderDecoded(@NonNull ImageDecoder decoder, @NonNull ImageInfo info, @NonNull Source source); }; /** @removed * @deprecated Replaced by {@link #DecodeException#SOURCE_EXCEPTION}. */ @Deprecated public static final int ERROR_SOURCE_EXCEPTION = 1; /** @removed * @deprecated Replaced by {@link #DecodeException#SOURCE_INCOMPLETE}. */ @Deprecated public static final int ERROR_SOURCE_INCOMPLETE = 2; /** @removed * @deprecated Replaced by {@link #DecodeException#SOURCE_MALFORMED_DATA}. */ @Deprecated public static final int ERROR_SOURCE_ERROR = 3; /** * 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 ""; } } } /** * Interface for inspecting a {@link DecodeException DecodeException} * and potentially preventing it from being thrown. * * <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 static interface OnPartialImageListener { /** * Called by {@link ImageDecoder} when there is only a partial image to * display. * * <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 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(@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"); } @Override protected void finalize() throws Throwable { try { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } // Avoid closing these in finalizer. mInputStream = null; mAssetFd = null; close(); } finally { super.finalize(); } } /** * 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. * @return a new Source object, which can be passed to * {@link #decodeDrawable decodeDrawable} or * {@link #decodeBitmap decodeBitmap}. */ @AnyThread @NonNull public static Source createSource(@NonNull Resources res, int resId) { return new ResourceSource(res, resId); } /** * 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 decodeDrawable} or * {@link #decodeBitmap decodeBitmap}. */ @AnyThread @NonNull public static Source createSource(@NonNull ContentResolver cr, @NonNull Uri 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 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("invalid offset/length!"); } return new ByteArraySource(data, offset, length); } /** * 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 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>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> * * @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); } /** * 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 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); } /** * Return the width and height of a given sample size. * * <p>This takes an input that functions like * {@link BitmapFactory.Options#inSampleSize}. It returns a width and * 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 #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) { 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; } /** * 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> * * <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}. */ @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; } /** * Set the target size with a sampleSize. * * <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>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 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. /** * Use the default allocation for the pixel memory. * * Will typically result in a {@link Bitmap.Config#HARDWARE} * allocation, but may be software for small images. In addition, this will * switch to software when HARDWARE is incompatible, e.g. * {@link #setMutableRequired setMutableRequired(true)} or * {@link #setDecodeAsAlphaMaskEnabled setDecodeAsAlphaMaskEnabled(true)}. */ public static final int ALLOCATOR_DEFAULT = 0; /** * Use a software allocation for the pixel memory. * * <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; /** * Use shared memory for the pixel memory. * * <p>Useful for sharing across processes. */ public static final int ALLOCATOR_SHARED_MEMORY = 2; /** * Require a {@link Bitmap.Config#HARDWARE} {@link Bitmap}. * * <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. * * <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 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. * * <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 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}. */ @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 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. * * <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 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 #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 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 void setCrop(@Nullable Rect subset) { mCropRect = subset; } /** * Return the cropping rectangle, if set. */ @Nullable public Rect getCrop() { return mCropRect; } /** * Set a Rect for retrieving nine patch padding. * * 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 void setOutPaddingRect(@NonNull Rect outPadding) { mOutPaddingRect = outPadding; } /** * Specify whether the {@link Bitmap} should be mutable. * * <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 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; } /** * 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. * * <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 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; } /** * Specify whether to potentially treat the output as an alpha mask. * * <p>If this is set to {@code true} and the image is encoded in a format * with only one channel, treat that channel as alpha. Otherwise this call has * no effect.</p> * * <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 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 { 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); } } /** * 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 decodeDrawableImpl(src, null); } /** * Create a {@link Bitmap} from a {@code Source}. * * @param src representing the encoded image. * @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 { 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; } // 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); } /** * 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 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); }