Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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.
*
* <p>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.
*
* <p>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).
*
* <p>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).
*
* <p>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.
*
* <p>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).
*
* <p>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.
*
* <p>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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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}.
*
* <p>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);
Expand Down
5 changes: 4 additions & 1 deletion android_jni/avifandroidjni/src/main/jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading