diff --git a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderTest.java b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderTest.java index e20b35986f..9bdead0c56 100644 --- a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderTest.java +++ b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderTest.java @@ -5,6 +5,8 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; +import android.hardware.HardwareBuffer; +import android.os.Build; import androidx.test.platform.app.InstrumentationRegistry; import java.io.IOException; import java.io.InputStream; @@ -256,6 +258,78 @@ public void testDecodeRegularClass() throws IOException { decoder.release(); } + // Tests hardware-bitmap decode for still images. Runs only once per image (skips when + // config != ARGB_8888 to avoid redundant iterations over the same image). + @Test + public void testDecodeHardwareBitmap() throws IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + if (image.isAnimated || config != Config.ARGB_8888) { + return; + } + ByteBuffer buffer = image.getBuffer(); + assertThat(buffer).isNotNull(); + // Test SDR path (R8G8B8A8). + Bitmap bitmap = AvifDecoder.decodeHardwareBitmap(buffer, buffer.remaining()); + assertThat(bitmap).isNotNull(); + assertThat(bitmap.getConfig()).isEqualTo(Config.HARDWARE); + assertThat(bitmap.getWidth()).isEqualTo(image.width); + assertThat(bitmap.getHeight()).isEqualTo(image.height); + // For >8-bit images, also test the HDR path (FP16). + if (image.depth > 8) { + buffer.rewind(); + Bitmap hdrBitmap = AvifDecoder.decodeHardwareBitmap(buffer, buffer.remaining(), 1, + /* allowHdr= */ true); + assertThat(hdrBitmap).isNotNull(); + assertThat(hdrBitmap.getConfig()).isEqualTo(Config.HARDWARE); + } + } + + // Tests hardware-bitmap decode for animated images. + @Test + public void testDecodeAnimatedHardwareBitmap() throws IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + if (!image.isAnimated || config != Config.ARGB_8888) { + return; + } + ByteBuffer buffer = image.getBuffer(); + AvifDecoder decoder = AvifDecoder.create(buffer, image.threads); + assertThat(decoder).isNotNull(); + // Test with allowHdr=true to exercise the FP16 path for >8-bit animated images. + boolean allowHdr = image.depth > 8; + for (int i = 0; i < image.frameCount; i++) { + Bitmap bitmap = decoder.nextFrameHardwareBitmap(allowHdr); + assertThat(bitmap).isNotNull(); + assertThat(bitmap.getConfig()).isEqualTo(Config.HARDWARE); + assertThat(bitmap.getWidth()).isEqualTo(image.width); + assertThat(bitmap.getHeight()).isEqualTo(image.height); + } + // Test nthFrameHardwareBitmap. + Bitmap bitmap = decoder.nthFrameHardwareBitmap(0, allowHdr); + assertThat(bitmap).isNotNull(); + assertThat(bitmap.getConfig()).isEqualTo(Config.HARDWARE); + + // Test buffer-reuse path: allocate once, decode all frames into the same buffer. + HardwareBuffer hwb = decoder.createHardwareBuffer(allowHdr); + assertThat(hwb).isNotNull(); + Bitmap reuseBitmap = Bitmap.wrapHardwareBuffer(hwb, null); + assertThat(reuseBitmap).isNotNull(); + assertThat(reuseBitmap.getConfig()).isEqualTo(Config.HARDWARE); + for (int i = 0; i < image.frameCount; i++) { + Bitmap result = decoder.nthFrameHardwareBitmap(i, allowHdr, hwb); + assertThat(result).isNotNull(); + assertThat(result.getConfig()).isEqualTo(Config.HARDWARE); + } + // Also verify nextFrameHardwareBitmap with dest: seek to frame 0, advance to frame 1. + decoder.nthFrameHardwareBitmap(0, allowHdr, hwb); + assertThat(decoder.nextFrameHardwareBitmap(allowHdr, hwb)).isNotNull(); + hwb.close(); + decoder.release(); + } + @Test public void testUtilityFunctions() throws IOException { // Test the avifResult value whose value and string representations are least likely to change. diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java index ee8c07e843..983291552b 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java @@ -4,7 +4,12 @@ package org.aomedia.avif.android; import android.graphics.Bitmap; +import android.hardware.HardwareBuffer; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.view.Display; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import java.nio.ByteBuffer; /** @@ -253,6 +258,213 @@ public int nthFrame(int n, Bitmap bitmap) { */ public static native String versionString(); + /** + * Returns true if {@code display} supports high bit-depth (HDR) rendering. + * + *
Pass the result to {@link #decodeHardwareBitmap(ByteBuffer, int, int, boolean)} as + * {@code allowHdr}: on SDR displays this avoids allocating an FP16 buffer; on HDR displays it + * preserves the full colour range of >8-bit AVIF images. + * + *
Requires API 24; always returns false on older devices. + * + * @param display The display to query (typically {@code WindowManager.getDefaultDisplay()} or + * a display obtained from {@link DisplayManager}). + * @return true if the display can render HDR content. + */ + @RequiresApi(24) + public static boolean isHighBitDepthDisplaySupported(Display display) { + if (Build.VERSION.SDK_INT < 24) return false; + if (Build.VERSION.SDK_INT >= 26 && display.isWideColorGamut()) return true; + Display.HdrCapabilities caps = display.getHdrCapabilities(); + return caps != null && caps.getSupportedHdrTypes().length > 0; + } + + /** + * Decodes a still AVIF image and returns a hardware-backed {@link Bitmap} (Config.HARDWARE). + * + *
The returned Bitmap is GPU-resident and cannot be modified. Returns null if the device does + * not support AHardwareBuffer (API < 26) or if the decode fails. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. + * @param length Length of the encoded buffer. + * @param threads Number of decode threads (0 = library default, negative = CPU core count). + * @param allowHdr When true and the image has depth > 8, an R16G16B16A16_FLOAT (FP16) buffer is + * used to preserve HDR precision. When false, R8G8B8A8_UNORM is always used (SDR). Use + * {@link #isHighBitDepthDisplaySupported} to determine the right value. + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public static Bitmap decodeHardwareBitmap(ByteBuffer encoded, int length, int threads, + boolean allowHdr) { + if (Build.VERSION.SDK_INT < 26) { + return null; + } + return (Bitmap) nativeDecodeHardwareBitmap(encoded, length, threads, allowHdr); + } + + /** + * Decodes a still AVIF image and returns a hardware-backed {@link Bitmap} (Config.HARDWARE). + * + *
The returned Bitmap is GPU-resident and cannot be modified. Returns null if the device does + * not support AHardwareBuffer (API < 26) or if the decode fails. + * + *
Always uses R8G8B8A8_UNORM (SDR). For HDR-aware decoding, use + * {@link #decodeHardwareBitmap(ByteBuffer, int, int, boolean)}. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. + * @param length Length of the encoded buffer. + * @param threads Number of decode threads (0 = library default, negative = CPU core count). + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public static Bitmap decodeHardwareBitmap(ByteBuffer encoded, int length, int threads) { + return decodeHardwareBitmap(encoded, length, threads, /* allowHdr= */ false); + } + + /** + * Decodes a still AVIF image and returns a hardware-backed {@link Bitmap} (Config.HARDWARE). + * + *
Uses a single decode thread and R8G8B8A8_UNORM (SDR). Returns null on failure. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. + * @param length Length of the encoded buffer. + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public static Bitmap decodeHardwareBitmap(ByteBuffer encoded, int length) { + return decodeHardwareBitmap(encoded, length, /* threads= */ 1, /* allowHdr= */ false); + } + + /** + * Allocates a {@link HardwareBuffer} compatible with this decoder's image for use with + * {@link #nextFrameHardwareBitmap(boolean, HardwareBuffer)} across animation frames. + * + *
Reuse the same buffer each frame: a {@link Bitmap} wrapping it via + * {@link Bitmap#wrapHardwareBuffer} reflects new content without re-allocation. + * The caller is responsible for closing the buffer when done. + * + * @param allowHdr When true, prefer R16G16B16A16_FLOAT for >8-bit images (falls back to + * R8G8B8A8_UNORM if unsupported). When false, always uses R8G8B8A8_UNORM. + * @return A new HardwareBuffer, or null on failure. + */ + @RequiresApi(26) + @Nullable + public HardwareBuffer createHardwareBuffer(boolean allowHdr) { + if (Build.VERSION.SDK_INT < 26) return null; + return (HardwareBuffer) nativeCreateHardwareBuffer(width, height, depth, allowHdr); + } + + /** + * Decodes the next frame of an animated AVIF and returns a hardware-backed {@link Bitmap}. + * + *
If {@code dest} is non-null, decodes into that buffer and wraps it as a Bitmap — the + * same Bitmap created via {@link Bitmap#wrapHardwareBuffer} reflects the new content without + * re-allocation. If {@code dest} is null, a new {@link HardwareBuffer} is allocated internally. + * + * @param allowHdr When true and the image has depth > 8, FP16 is used. See + * {@link #decodeHardwareBitmap(ByteBuffer, int, int, boolean)}. + * @param dest Optional pre-allocated buffer to decode into. Must match image dimensions. + * Use {@link #createHardwareBuffer} to allocate a compatible buffer. + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public Bitmap nextFrameHardwareBitmap(boolean allowHdr, @Nullable HardwareBuffer dest) { + if (Build.VERSION.SDK_INT < 26) return null; + return (Bitmap) nativeNextFrameHardwareBitmap(decoder, allowHdr, dest); + } + + /** + * Decodes the next frame of an animated AVIF and returns a hardware-backed {@link Bitmap}. + * + *
Allocates a new {@link HardwareBuffer} internally on each call. For zero-copy frame + * reuse, use {@link #nextFrameHardwareBitmap(boolean, HardwareBuffer)} instead. + * + * @param allowHdr When true and the image has depth > 8, FP16 is used. + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public Bitmap nextFrameHardwareBitmap(boolean allowHdr) { + return nextFrameHardwareBitmap(allowHdr, /* dest= */ null); + } + + /** + * Decodes the next frame of an animated AVIF and returns a hardware-backed {@link Bitmap}. + * + *
Uses R8G8B8A8_UNORM (SDR). Returns null on failure. + * + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public Bitmap nextFrameHardwareBitmap() { + return nextFrameHardwareBitmap(/* allowHdr= */ false, /* dest= */ null); + } + + /** + * Decodes the nth frame of an animated AVIF and returns a hardware-backed {@link Bitmap}. + * + *
If {@code dest} is non-null, decodes into that buffer and wraps it as a Bitmap. If + * {@code dest} is null, a new {@link HardwareBuffer} is allocated internally. + * + * @param n The zero-based index of the frame to decode. + * @param allowHdr When true and the image has depth > 8, FP16 is used. + * @param dest Optional pre-allocated buffer to decode into. Must match image dimensions. + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public Bitmap nthFrameHardwareBitmap(int n, boolean allowHdr, @Nullable HardwareBuffer dest) { + if (Build.VERSION.SDK_INT < 26) return null; + return (Bitmap) nativeNthFrameHardwareBitmap(decoder, n, allowHdr, dest); + } + + /** + * Decodes the nth frame of an animated AVIF and returns a hardware-backed {@link Bitmap}. + * + *
Allocates a new {@link HardwareBuffer} internally. For zero-copy reuse, use + * {@link #nthFrameHardwareBitmap(int, boolean, HardwareBuffer)} instead. + * + * @param n The zero-based index of the frame to decode. + * @param allowHdr When true and the image has depth > 8, FP16 is used. + * @return A hardware-backed Bitmap on success, null on failure. + */ + @RequiresApi(26) + @Nullable + public Bitmap nthFrameHardwareBitmap(int n, boolean allowHdr) { + return nthFrameHardwareBitmap(n, allowHdr, /* dest= */ null); + } + + /** + * Decodes the nth frame of an animated AVIF and returns a hardware-backed {@link Bitmap}. + * + *
Uses R8G8B8A8_UNORM (SDR). Returns null on failure.
+ *
+ * @param n The zero-based index of the frame to decode.
+ * @return A hardware-backed Bitmap on success, null on failure.
+ */
+ @RequiresApi(26)
+ @Nullable
+ public Bitmap nthFrameHardwareBitmap(int n) {
+ return nthFrameHardwareBitmap(n, /* allowHdr= */ false, /* dest= */ null);
+ }
+
+ private static native Object nativeDecodeHardwareBitmap(
+ ByteBuffer encoded, int length, int threads, boolean allowHdr);
+
+ private native Object nativeNextFrameHardwareBitmap(
+ long decoder, boolean allowHdr, Object dest);
+
+ private native Object nativeNthFrameHardwareBitmap(
+ long decoder, int n, boolean allowHdr, Object dest);
+
+ private native Object nativeCreateHardwareBuffer(
+ int width, int height, int depth, boolean allowHdr);
+
private native long createDecoder(ByteBuffer encoded, int length, int threads);
private native void destroyDecoder(long decoder);
diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt
index da944b3638..778b4bddd0 100644
--- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt
+++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt
@@ -40,4 +40,7 @@ include_directories(${CPU_FEATURES_DIR})
add_library(cpufeatures STATIC "${CPU_FEATURES_DIR}/cpu-features.c")
target_link_options(avif_android PRIVATE "-Wl,-z,max-page-size=16384")
-target_link_libraries(avif_android jnigraphics avif log cpufeatures)
+# Make AHardwareBuffer symbols weak so they are absent (null) on API < 26 rather
+# than causing a dlopen failure at load time.
+target_compile_definitions(avif_android PRIVATE __ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__)
+target_link_libraries(avif_android android jnigraphics avif log cpufeatures)
diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
index 83eb3e3d93..c7d21bb207 100644
--- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
+++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc
@@ -1,7 +1,10 @@
// Copyright 2022 Google LLC
// SPDX-License-Identifier: BSD-2-Clause
+#include