2181 lines
82 KiB
Java
2181 lines
82 KiB
Java
/*
|
|
* 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.media.MediaCodecInfo;
|
|
import android.media.MediaCodecList;
|
|
import android.media.MediaFormat;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.os.Trace;
|
|
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.Locale;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.Callable;
|
|
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 Uri}, call
|
|
* {@link #createSource(ContentResolver, Uri)} 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 {
|
|
/**
|
|
* 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}
|
|
* can even be used simultaneously in multiple threads.</p>
|
|
*/
|
|
public static abstract class Source {
|
|
private Source() {}
|
|
|
|
@Nullable
|
|
Resources getResources() { return null; }
|
|
|
|
int getDensity() { return Bitmap.DENSITY_NONE; }
|
|
|
|
final int computeDstDensity() {
|
|
Resources res = getResources();
|
|
if (res == null) {
|
|
return Bitmap.getDefaultDensity();
|
|
}
|
|
|
|
return res.getDisplayMetrics().densityDpi;
|
|
}
|
|
|
|
@NonNull
|
|
abstract ImageDecoder createImageDecoder(boolean preferAnimation) 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(boolean preferAnimation) throws IOException {
|
|
return nCreate(mData, mOffset, mLength, preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "ByteArraySource{len=" + mLength + "}";
|
|
}
|
|
}
|
|
|
|
private static class ByteBufferSource extends Source {
|
|
ByteBufferSource(@NonNull ByteBuffer buffer) {
|
|
mBuffer = buffer;
|
|
mLength = mBuffer.limit() - mBuffer.position();
|
|
}
|
|
|
|
private final ByteBuffer mBuffer;
|
|
private final int mLength;
|
|
|
|
@Override
|
|
public ImageDecoder createImageDecoder(boolean preferAnimation) 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, preferAnimation, this);
|
|
}
|
|
ByteBuffer buffer = mBuffer.slice();
|
|
return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "ByteBufferSource{len=" + mLength + "}";
|
|
}
|
|
}
|
|
|
|
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(boolean preferAnimation) throws IOException {
|
|
AssetFileDescriptor assetFd = null;
|
|
try {
|
|
if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme())) {
|
|
assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
|
|
"image/*", null);
|
|
} else {
|
|
assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
|
|
}
|
|
} catch (FileNotFoundException e) {
|
|
// Handled below, along with the case where assetFd was set to null.
|
|
}
|
|
|
|
if (assetFd == null) {
|
|
// 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, preferAnimation, this);
|
|
}
|
|
|
|
return createFromAssetFileDescriptor(assetFd, preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
String uri = mUri.toString();
|
|
if (uri.length() > 90) {
|
|
// We want to keep the Uri usable - usually the authority and the end is important.
|
|
uri = uri.substring(0, 80) + ".." + uri.substring(uri.length() - 10);
|
|
}
|
|
return "ContentResolverSource{uri=" + uri + "}";
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
private static ImageDecoder createFromFile(@NonNull File file,
|
|
boolean preferAnimation, @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, preferAnimation, source);
|
|
}
|
|
|
|
ImageDecoder decoder = null;
|
|
try {
|
|
decoder = nCreate(fd, AssetFileDescriptor.UNKNOWN_LENGTH, preferAnimation, 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, boolean preferAnimation, Source source) throws IOException {
|
|
// Arbitrary size matches BitmapFactory.
|
|
byte[] storage = new byte[16 * 1024];
|
|
ImageDecoder decoder = null;
|
|
try {
|
|
decoder = nCreate(is, storage, preferAnimation, source);
|
|
} finally {
|
|
if (decoder == null) {
|
|
if (closeInputStream) {
|
|
IoUtils.closeQuietly(is);
|
|
}
|
|
} else {
|
|
decoder.mInputStream = is;
|
|
decoder.mOwnsInputStream = closeInputStream;
|
|
decoder.mTempStorage = storage;
|
|
}
|
|
}
|
|
|
|
return decoder;
|
|
}
|
|
|
|
@NonNull
|
|
private static ImageDecoder createFromAssetFileDescriptor(@NonNull AssetFileDescriptor assetFd,
|
|
boolean preferAnimation, Source source) throws IOException {
|
|
if (assetFd == null) {
|
|
throw new FileNotFoundException();
|
|
}
|
|
final FileDescriptor fd = assetFd.getFileDescriptor();
|
|
final long offset = assetFd.getStartOffset();
|
|
|
|
ImageDecoder decoder = null;
|
|
try {
|
|
try {
|
|
Os.lseek(fd, offset, SEEK_SET);
|
|
decoder = nCreate(fd, assetFd.getDeclaredLength(), preferAnimation, source);
|
|
} catch (ErrnoException e) {
|
|
decoder = createFromStream(new FileInputStream(fd), true, preferAnimation, source);
|
|
}
|
|
} finally {
|
|
if (decoder == null) {
|
|
IoUtils.closeQuietly(assetFd);
|
|
} else {
|
|
decoder.mAssetFd = assetFd;
|
|
}
|
|
}
|
|
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, @NonNull InputStream is, int inputDensity) {
|
|
if (is == null) {
|
|
throw new IllegalArgumentException("The InputStream cannot be null");
|
|
}
|
|
mResources = res;
|
|
mInputStream = is;
|
|
mInputDensity = inputDensity;
|
|
}
|
|
|
|
final Resources mResources;
|
|
InputStream mInputStream;
|
|
final int mInputDensity;
|
|
|
|
@Override
|
|
public Resources getResources() { return mResources; }
|
|
|
|
@Override
|
|
public int getDensity() { return mInputDensity; }
|
|
|
|
@Override
|
|
public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
|
|
|
|
synchronized (this) {
|
|
if (mInputStream == null) {
|
|
throw new IOException("Cannot reuse InputStreamSource");
|
|
}
|
|
InputStream is = mInputStream;
|
|
mInputStream = null;
|
|
return createFromStream(is, false, preferAnimation, this);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "InputStream{s=" + mInputStream + "}";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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(boolean preferAnimation) throws IOException {
|
|
synchronized (this) {
|
|
if (mAssetInputStream == null) {
|
|
throw new IOException("Cannot reuse AssetInputStreamSource");
|
|
}
|
|
AssetInputStream ais = mAssetInputStream;
|
|
mAssetInputStream = null;
|
|
return createFromAsset(ais, preferAnimation, this);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "AssetInputStream{s=" + mAssetInputStream + "}";
|
|
}
|
|
}
|
|
|
|
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(boolean preferAnimation) 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, preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
// Try to return a human-readable name for debugging purposes.
|
|
try {
|
|
return "Resource{name=" + mResources.getResourceName(mResId) + "}";
|
|
} catch (Resources.NotFoundException e) {
|
|
// It's ok if we don't find it, fall back to ID.
|
|
}
|
|
return "Resource{id=" + mResId + "}";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ImageDecoder will own the AssetInputStream.
|
|
*/
|
|
private static ImageDecoder createFromAsset(AssetInputStream ais,
|
|
boolean preferAnimation, Source source) throws IOException {
|
|
ImageDecoder decoder = null;
|
|
try {
|
|
long asset = ais.getNativeAsset();
|
|
decoder = nCreate(asset, preferAnimation, 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(boolean preferAnimation) throws IOException {
|
|
InputStream is = mAssets.open(mFileName);
|
|
return createFromAsset((AssetInputStream) is, preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "AssetSource{file=" + mFileName + "}";
|
|
}
|
|
}
|
|
|
|
private static class FileSource extends Source {
|
|
FileSource(@NonNull File file) {
|
|
mFile = file;
|
|
}
|
|
|
|
private final File mFile;
|
|
|
|
@Override
|
|
public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
|
|
return createFromFile(mFile, preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "FileSource{file=" + mFile + "}";
|
|
}
|
|
}
|
|
|
|
private static class CallableSource extends Source {
|
|
CallableSource(@NonNull Callable<AssetFileDescriptor> callable) {
|
|
mCallable = callable;
|
|
}
|
|
|
|
private final Callable<AssetFileDescriptor> mCallable;
|
|
|
|
@Override
|
|
public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
|
|
AssetFileDescriptor assetFd = null;
|
|
try {
|
|
assetFd = mCallable.call();
|
|
} catch (Exception e) {
|
|
if (e instanceof IOException) {
|
|
throw (IOException) e;
|
|
} else {
|
|
throw new IOException(e);
|
|
}
|
|
}
|
|
return createFromAssetFileDescriptor(assetFd, preferAnimation, this);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "CallableSource{obj=" + mCallable.toString() + "}";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Information about an encoded image.
|
|
*/
|
|
public static class ImageInfo {
|
|
private final Size mSize;
|
|
private final boolean mIsAnimated;
|
|
private final String mMimeType;
|
|
private final ColorSpace mColorSpace;
|
|
|
|
private ImageInfo(
|
|
@NonNull Size size,
|
|
boolean isAnimated,
|
|
@NonNull String mimeType,
|
|
@Nullable ColorSpace colorSpace) {
|
|
mSize = size;
|
|
mIsAnimated = isAnimated;
|
|
mMimeType = mimeType;
|
|
mColorSpace = colorSpace;
|
|
}
|
|
|
|
/**
|
|
* Size of the image, without scaling or cropping.
|
|
*/
|
|
@NonNull
|
|
public Size getSize() {
|
|
return mSize;
|
|
}
|
|
|
|
/**
|
|
* The mimeType of the image.
|
|
*/
|
|
@NonNull
|
|
public String getMimeType() {
|
|
return mMimeType;
|
|
}
|
|
|
|
/**
|
|
* Whether the image is animated.
|
|
*
|
|
* <p>If {@code true}, {@link #decodeDrawable decodeDrawable} will
|
|
* return an {@link AnimatedImageDrawable}.</p>
|
|
*/
|
|
public boolean isAnimated() {
|
|
return mIsAnimated;
|
|
}
|
|
|
|
/**
|
|
* 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 mColorSpace;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 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);
|
|
|
|
};
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return if the given MIME type is a supported file format that can be
|
|
* decoded by this class. This can be useful to determine if a file can be
|
|
* decoded directly, or if it needs to be converted into a more general
|
|
* format using an API like {@link ContentResolver#openTypedAssetFile}.
|
|
*/
|
|
public static boolean isMimeTypeSupported(@NonNull String mimeType) {
|
|
Objects.requireNonNull(mimeType);
|
|
switch (mimeType.toLowerCase(Locale.US)) {
|
|
case "image/png":
|
|
case "image/jpeg":
|
|
case "image/webp":
|
|
case "image/gif":
|
|
case "image/bmp":
|
|
case "image/x-ico":
|
|
case "image/vnd.wap.wbmp":
|
|
case "image/x-sony-arw":
|
|
case "image/x-canon-cr2":
|
|
case "image/x-adobe-dng":
|
|
case "image/x-nikon-nef":
|
|
case "image/x-nikon-nrw":
|
|
case "image/x-olympus-orf":
|
|
case "image/x-fuji-raf":
|
|
case "image/x-panasonic-rw2":
|
|
case "image/x-pentax-pef":
|
|
case "image/x-samsung-srw":
|
|
return true;
|
|
case "image/heif":
|
|
case "image/heic":
|
|
return isHevcDecoderSupported();
|
|
case "image/avif":
|
|
return isP010SupportedForAV1();
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* <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 {@code data}, so its contents must not
|
|
* be modified, even after the {@code AnimatedImageDrawable} is returned.
|
|
* {@code data}'s contents should never be modified during decode.</p>
|
|
*
|
|
* @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.
|
|
*/
|
|
@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);
|
|
}
|
|
|
|
/**
|
|
* Create a new {@link Source Source} from a byte array.
|
|
*
|
|
* <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 {@code data}, so its contents must not
|
|
* be modified, even after the {@code AnimatedImageDrawable} is returned.
|
|
* {@code data}'s contents should never be modified during decode.</p>
|
|
*
|
|
* @param data byte array of compressed image data.
|
|
* @return a new Source object, which can be passed to
|
|
* {@link #decodeDrawable decodeDrawable} or
|
|
* {@link #decodeBitmap decodeBitmap}.
|
|
* @throws NullPointerException if data is null.
|
|
*/
|
|
@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, @NonNull 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, @NonNull InputStream is, int density) {
|
|
return new InputStreamSource(res, is, density);
|
|
}
|
|
|
|
/**
|
|
* Create a new {@link Source Source} from a {@link java.io.File}.
|
|
* <p>
|
|
* This method should only be used for files that you have direct access to;
|
|
* if you'd like to work with files hosted outside your app, use an API like
|
|
* {@link #createSource(Callable)} or
|
|
* {@link #createSource(ContentResolver, Uri)}.
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
* Create a new {@link Source Source} from a {@link Callable} that returns a
|
|
* new {@link AssetFileDescriptor} for each request. This provides control
|
|
* over how the {@link AssetFileDescriptor} is created, such as passing
|
|
* options into {@link ContentResolver#openTypedAssetFileDescriptor}, or
|
|
* enabling use of a {@link android.os.CancellationSignal}.
|
|
* <p>
|
|
* It's important for the given {@link Callable} to return a new, unique
|
|
* {@link AssetFileDescriptor} for each invocation, to support reuse of the
|
|
* returned {@link Source Source}.
|
|
*
|
|
* @return a new Source object, which can be passed to
|
|
* {@link #decodeDrawable decodeDrawable} or {@link #decodeBitmap
|
|
* decodeBitmap}.
|
|
*/
|
|
@AnyThread
|
|
@NonNull
|
|
public static Source createSource(@NonNull Callable<AssetFileDescriptor> callable) {
|
|
return new CallableSource(callable);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@NonNull
|
|
private 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
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Return whether the {@link Bitmap} will have unpremultiplied pixels.
|
|
*/
|
|
public boolean isUnpremultipliedRequired() {
|
|
return mUnpremultipliedRequired;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* Must be public for access from android.graphics.drawable,
|
|
* but must not be called from outside the UI module.
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Return whether the decoded {@link Bitmap} will be mutable.
|
|
*/
|
|
public boolean isMutableRequired() {
|
|
return mMutable;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* 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 and
|
|
* {@link ColorSpace.Named#EXTENDED_SRGB EXTENDED_SRGB} for
|
|
* {@link Bitmap.Config#RGBA_F16}).</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">Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
|
|
* 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.
|
|
* Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE},
|
|
* the color spaces with non ICC parametric curve transfer function are allowed.
|
|
* E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.
|
|
* </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(boolean animated) {
|
|
if (mNativePtr == 0) {
|
|
throw new IllegalStateException("Cannot use closed ImageDecoder!");
|
|
}
|
|
|
|
checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
|
|
|
|
// animated ignores the allocator, so no need to check for incompatible
|
|
// fields.
|
|
if (!animated && 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!");
|
|
}
|
|
}
|
|
|
|
private static void checkSubset(int width, int height, Rect r) {
|
|
if (r == null) {
|
|
return;
|
|
}
|
|
if (r.width() <= 0 || r.height() <= 0) {
|
|
throw new IllegalStateException("Subset " + r + " is empty/unsorted");
|
|
}
|
|
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 + ")");
|
|
}
|
|
}
|
|
|
|
private boolean checkForExtended() {
|
|
if (mDesiredColorSpace == null) {
|
|
return false;
|
|
}
|
|
return mDesiredColorSpace == ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)
|
|
|| mDesiredColorSpace == ColorSpace.get(ColorSpace.Named.LINEAR_EXTENDED_SRGB);
|
|
}
|
|
|
|
private long getColorSpacePtr() {
|
|
if (mDesiredColorSpace == null) {
|
|
return 0;
|
|
}
|
|
return mDesiredColorSpace.getNativeInstance();
|
|
}
|
|
|
|
@WorkerThread
|
|
@NonNull
|
|
private Bitmap decodeBitmapInternal() throws IOException {
|
|
checkState(false);
|
|
return nDecodeBitmap(mNativePtr, this, mPostProcessor != null,
|
|
mDesiredWidth, mDesiredHeight, mCropRect,
|
|
mMutable, mAllocator, mUnpremultipliedRequired,
|
|
mConserveMemory, mDecodeAsAlphaMask, getColorSpacePtr(),
|
|
checkForExtended());
|
|
}
|
|
|
|
private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener,
|
|
@NonNull Source src) {
|
|
if (listener != null) {
|
|
ImageInfo info =
|
|
new ImageInfo(
|
|
new Size(mWidth, mHeight), mAnimated, getMimeType(), getColorSpace());
|
|
listener.onHeaderDecoded(this, info, src);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return {@link ImageInfo} from a {@code Source}.
|
|
*
|
|
* <p>Returns the same {@link ImageInfo} object that a usual decoding process would return as
|
|
* part of {@link OnHeaderDecodedListener}.
|
|
*
|
|
* @param src representing the encoded image.
|
|
* @return ImageInfo describing the image.
|
|
* @throws IOException if {@code src} is not found, is an unsupported format, or cannot be
|
|
* decoded for any reason.
|
|
* @hide
|
|
*/
|
|
@WorkerThread
|
|
@NonNull
|
|
public static ImageInfo decodeHeader(@NonNull Source src) throws IOException {
|
|
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeHeader");
|
|
try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) {
|
|
// We don't want to leak decoder so resolve all properties immediately.
|
|
return new ImageInfo(
|
|
new Size(decoder.mWidth, decoder.mHeight),
|
|
decoder.mAnimated,
|
|
decoder.getMimeType(),
|
|
decoder.getColorSpace());
|
|
} finally {
|
|
// Close the ImageDecoder#decodeHeader trace.
|
|
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeDrawable");
|
|
try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) {
|
|
decoder.mSource = src;
|
|
decoder.callHeaderDecoded(listener, src);
|
|
|
|
try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) {
|
|
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;
|
|
decoder.checkState(true);
|
|
Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
|
|
postProcessPtr, decoder.mDesiredWidth,
|
|
decoder.mDesiredHeight, decoder.getColorSpacePtr(),
|
|
decoder.checkForExtended(), 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);
|
|
}
|
|
} finally {
|
|
// Close the ImageDecoder#decode trace.
|
|
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeBitmap");
|
|
try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) {
|
|
decoder.mSource = src;
|
|
decoder.callHeaderDecoded(listener, src);
|
|
try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) {
|
|
// 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;
|
|
}
|
|
} finally {
|
|
// Close the ImageDecoder#decode trace.
|
|
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This describes the decoder in traces to ease debugging. It has to be called after
|
|
* header has been decoded and width/height have been populated. It should be used
|
|
* inside a try-with-resources call to automatically complete the trace.
|
|
*/
|
|
private static AutoCloseable traceDecoderSource(ImageDecoder decoder) {
|
|
final boolean resourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES);
|
|
if (resourceTracingEnabled) {
|
|
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder));
|
|
}
|
|
|
|
return new AutoCloseable() {
|
|
@Override
|
|
public void close() throws Exception {
|
|
if (resourceTracingEnabled) {
|
|
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// 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
|
|
&& Compatibility.getTargetSdkVersion() >= Build.VERSION_CODES.P) {
|
|
return srcDensity;
|
|
}
|
|
|
|
float scale = (float) dstDensity / srcDensity;
|
|
int scaledWidth = Math.max((int) (mWidth * scale + 0.5f), 1);
|
|
int scaledHeight = Math.max((int) (mHeight * scale + 0.5f), 1);
|
|
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 static boolean sIsHevcDecoderSupported = false;
|
|
private static boolean sIsHevcDecoderSupportedInitialized = false;
|
|
private static final Object sIsHevcDecoderSupportedLock = new Object();
|
|
|
|
/*
|
|
* Check if HEVC decoder is supported by the device.
|
|
*/
|
|
@SuppressWarnings("AndroidFrameworkCompatChange")
|
|
private static boolean isHevcDecoderSupported() {
|
|
synchronized (sIsHevcDecoderSupportedLock) {
|
|
if (sIsHevcDecoderSupportedInitialized) {
|
|
return sIsHevcDecoderSupported;
|
|
}
|
|
MediaFormat format = new MediaFormat();
|
|
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_HEVC);
|
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
|
sIsHevcDecoderSupported = mcl.findDecoderForFormat(format) != null;
|
|
sIsHevcDecoderSupportedInitialized = true;
|
|
return sIsHevcDecoderSupported;
|
|
}
|
|
}
|
|
|
|
private static boolean sIsP010SupportedForAV1 = false;
|
|
private static boolean sIsP010SupportedForHEVC = false;
|
|
private static boolean sIsP010SupportedFlagsInitialized = false;
|
|
private static final Object sIsP010SupportedLock = new Object();
|
|
|
|
/**
|
|
* Checks if the device supports decoding 10-bit AV1.
|
|
*/
|
|
@SuppressWarnings("AndroidFrameworkCompatChange") // This is not an app-visible API.
|
|
private static boolean isP010SupportedForAV1() {
|
|
synchronized (sIsP010SupportedLock) {
|
|
if (sIsP010SupportedFlagsInitialized) {
|
|
return sIsP010SupportedForAV1;
|
|
}
|
|
checkP010SupportforAV1HEVC();
|
|
return sIsP010SupportedForAV1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the device supports decoding 10-bit HEVC.
|
|
* This method is called by JNI.
|
|
*/
|
|
@SuppressWarnings("unused")
|
|
private static boolean isP010SupportedForHEVC() {
|
|
synchronized (sIsP010SupportedLock) {
|
|
if (sIsP010SupportedFlagsInitialized) {
|
|
return sIsP010SupportedForHEVC;
|
|
}
|
|
checkP010SupportforAV1HEVC();
|
|
return sIsP010SupportedForHEVC;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the device supports decoding 10-bit for the given mime type.
|
|
*/
|
|
private static void checkP010SupportforAV1HEVC() {
|
|
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
|
for (MediaCodecInfo mediaCodecInfo : codecList.getCodecInfos()) {
|
|
if (mediaCodecInfo.isEncoder()) {
|
|
continue;
|
|
}
|
|
for (String mediaType : mediaCodecInfo.getSupportedTypes()) {
|
|
if (mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)
|
|
|| mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
|
|
MediaCodecInfo.CodecCapabilities codecCapabilities =
|
|
mediaCodecInfo.getCapabilitiesForType(mediaType);
|
|
for (int i = 0; i < codecCapabilities.colorFormats.length; ++i) {
|
|
if (codecCapabilities.colorFormats[i]
|
|
== MediaCodecInfo.CodecCapabilities.COLOR_FormatYUVP010) {
|
|
if (mediaType.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) {
|
|
sIsP010SupportedForAV1 = true;
|
|
} else {
|
|
sIsP010SupportedForHEVC = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
sIsP010SupportedFlagsInitialized = true;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a short string describing what passed ImageDecoder is loading -
|
|
* it reports image dimensions, desired dimensions (if any) and source resource.
|
|
*
|
|
* The string appears in perf traces to simplify search for slow or memory intensive
|
|
* image loads.
|
|
*
|
|
* Example: ID#w=300;h=250;dw=150;dh=150;src=Resource{name=@resource}
|
|
*
|
|
* @hide
|
|
*/
|
|
private static String describeDecoderForTrace(@NonNull ImageDecoder decoder) {
|
|
StringBuilder builder = new StringBuilder();
|
|
// Source dimensions
|
|
builder.append("ID#w=");
|
|
builder.append(decoder.mWidth);
|
|
builder.append(";h=");
|
|
builder.append(decoder.mHeight);
|
|
// Desired dimensions (if present)
|
|
if (decoder.mDesiredWidth != decoder.mWidth
|
|
|| decoder.mDesiredHeight != decoder.mHeight) {
|
|
builder.append(";dw=");
|
|
builder.append(decoder.mDesiredWidth);
|
|
builder.append(";dh=");
|
|
builder.append(decoder.mDesiredHeight);
|
|
}
|
|
// Source description
|
|
builder.append(";src=");
|
|
builder.append(decoder.mSource);
|
|
return builder.toString();
|
|
}
|
|
|
|
/**
|
|
* Records a trace with information about the source being decoded - dimensions,
|
|
* desired dimensions and source information.
|
|
*
|
|
* It significantly eases debugging of slow resource loads on main thread and
|
|
* possible large memory consumers.
|
|
*
|
|
* @hide
|
|
*/
|
|
private static final class ImageDecoderSourceTrace implements AutoCloseable {
|
|
|
|
private final boolean mResourceTracingEnabled;
|
|
|
|
ImageDecoderSourceTrace(ImageDecoder decoder) {
|
|
mResourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES);
|
|
if (mResourceTracingEnabled) {
|
|
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void close() {
|
|
if (mResourceTracingEnabled) {
|
|
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static native ImageDecoder nCreate(long asset,
|
|
boolean preferAnimation, Source src) throws IOException;
|
|
private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit,
|
|
boolean preferAnimation, Source src) throws IOException;
|
|
private static native ImageDecoder nCreate(byte[] data, int offset, int length,
|
|
boolean preferAnimation, Source src) throws IOException;
|
|
private static native ImageDecoder nCreate(InputStream is, byte[] storage,
|
|
boolean preferAnimation, Source src) throws IOException;
|
|
// The fd must be seekable.
|
|
private static native ImageDecoder nCreate(FileDescriptor fd, long length,
|
|
boolean preferAnimation, 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,
|
|
long desiredColorSpace, boolean extended)
|
|
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);
|
|
}
|