diff --git a/core/tests/coretests/res/drawable-nodpi/animated_webp.webp b/core/tests/coretests/res/drawable-nodpi/animated_webp.webp new file mode 100644 index 0000000000000..2d28dbfd380b8 Binary files /dev/null and b/core/tests/coretests/res/drawable-nodpi/animated_webp.webp differ diff --git a/core/tests/coretests/src/android/graphics/ImageDecoderTest.java b/core/tests/coretests/src/android/graphics/ImageDecoderTest.java new file mode 100644 index 0000000000000..8b3e6ba283b2e --- /dev/null +++ b/core/tests/coretests/src/android/graphics/ImageDecoderTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2023 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 com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.frameworks.coretests.R; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ImageDecoderTest { + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + + @Test + public void onDecodeHeader_png_returnsPopulatedData() throws IOException { + ImageDecoder.Source src = + ImageDecoder.createSource(mContext.getResources(), R.drawable.gettysburg); + ImageDecoder.ImageInfo info = ImageDecoder.decodeHeader(src); + assertThat(info.getSize().getWidth()).isEqualTo(432); + assertThat(info.getSize().getHeight()).isEqualTo(291); + assertThat(info.getMimeType()).isEqualTo("image/png"); + assertThat(info.getColorSpace()).isNotNull(); + assertThat(info.getColorSpace().getModel()).isEqualTo(ColorSpace.Model.RGB); + assertThat(info.getColorSpace().getId()).isEqualTo(0); + assertThat(info.isAnimated()).isFalse(); + } + + @Test + public void onDecodeHeader_animatedWebP_returnsPopulatedData() throws IOException { + ImageDecoder.Source src = + ImageDecoder.createSource(mContext.getResources(), R.drawable.animated_webp); + ImageDecoder.ImageInfo info = ImageDecoder.decodeHeader(src); + assertThat(info.getSize().getWidth()).isEqualTo(278); + assertThat(info.getSize().getHeight()).isEqualTo(183); + assertThat(info.getMimeType()).isEqualTo("image/webp"); + assertThat(info.getColorSpace()).isNotNull(); + assertThat(info.getColorSpace().getModel()).isEqualTo(ColorSpace.Model.RGB); + assertThat(info.getColorSpace().getId()).isEqualTo(0); + assertThat(info.isAnimated()).isTrue(); + } + + @Test(expected = IOException.class) + public void onDecodeHeader_invalidSource_throwsException() throws IOException { + ImageDecoder.Source src = ImageDecoder.createSource(new File("/this/file/does/not/exist")); + ImageDecoder.decodeHeader(src); + } + + @Test(expected = IOException.class) + public void onDecodeHeader_invalidResource_throwsException() throws IOException { + ImageDecoder.Source src = + ImageDecoder.createSource(mContext.getResources(), R.drawable.box); + ImageDecoder.decodeHeader(src); + } +} diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index dd4b58eb83dc0..b2da233fc21ec 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -627,11 +627,19 @@ public final class ImageDecoder implements AutoCloseable { */ public static class ImageInfo { private final Size mSize; - private ImageDecoder mDecoder; + private final boolean mIsAnimated; + private final String mMimeType; + private final ColorSpace mColorSpace; - private ImageInfo(@NonNull ImageDecoder decoder) { - mSize = new Size(decoder.mWidth, decoder.mHeight); - mDecoder = decoder; + private ImageInfo( + @NonNull Size size, + boolean isAnimated, + @NonNull String mimeType, + @Nullable ColorSpace colorSpace) { + mSize = size; + mIsAnimated = isAnimated; + mMimeType = mimeType; + mColorSpace = colorSpace; } /** @@ -647,7 +655,7 @@ public final class ImageDecoder implements AutoCloseable { */ @NonNull public String getMimeType() { - return mDecoder.getMimeType(); + return mMimeType; } /** @@ -657,7 +665,7 @@ public final class ImageDecoder implements AutoCloseable { * return an {@link AnimatedImageDrawable}.

*/ public boolean isAnimated() { - return mDecoder.mAnimated; + return mIsAnimated; } /** @@ -669,7 +677,7 @@ public final class ImageDecoder implements AutoCloseable { */ @Nullable public ColorSpace getColorSpace() { - return mDecoder.getColorSpace(); + return mColorSpace; } }; @@ -1798,12 +1806,39 @@ public final class ImageDecoder implements AutoCloseable { 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; - } + ImageInfo info = + new ImageInfo( + new Size(mWidth, mHeight), mAnimated, getMimeType(), getColorSpace()); + listener.onHeaderDecoded(this, info, src); + } + } + + /** + * Return {@link ImageInfo} from a {@code Source}. + * + *

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