From 5aa8489fa157b0ff6e505c38fc1a63f308aaee56 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 11 Jul 2026 22:48:07 +0900 Subject: [PATCH 01/25] =?UTF-8?q?Hardware=20Buffer=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/README.md | 3 + .../aomedia/avif/android/AvifDecoderTest.java | 71 ++++ .../org/aomedia/avif/android/AvifDecoder.java | 100 +++++- .../src/main/jni/CMakeLists.txt | 2 +- .../src/main/jni/libavif_jni.cc | 340 +++++++++++++----- cmake/Modules/LocalDav1d.cmake | 6 +- ext/dav1d_android.sh | 5 +- 7 files changed, 439 insertions(+), 88 deletions(-) diff --git a/android_jni/README.md b/android_jni/README.md index 3f67047651..bc32e9b409 100644 --- a/android_jni/README.md +++ b/android_jni/README.md @@ -39,6 +39,9 @@ $ ./dav1d_android.sh "${ANDROID_NDK_HOME}" $ cd .. ``` +The Android dav1d build is configured with `-Dbitdepths=8` (8-bit AV1 only). Re-run the +script after changing this option so that all ABIs are rebuilt. + If you want to use libgav1 instead: ``` 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..fabc835026 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,9 @@ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; +import android.graphics.ColorSpace; +import android.hardware.HardwareBuffer; +import android.os.Build; import androidx.test.platform.app.InstrumentationRegistry; import java.io.IOException; import java.io.InputStream; @@ -256,6 +259,74 @@ public void testDecodeRegularClass() throws IOException { decoder.release(); } + @Test + public void testDecodeToHardwareBuffer() throws IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return; + } + if (image.isAnimated || config != Config.ARGB_8888) { + return; + } + ByteBuffer buffer = image.getBuffer(); + assertThat(buffer).isNotNull(); + Info info = new Info(); + assertThat(AvifDecoder.getInfo(buffer, buffer.remaining(), info)).isTrue(); + + HardwareBuffer hardwareBuffer = + AvifDecoder.decodeToHardwareBuffer(buffer, buffer.remaining(), image.threads); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getWidth()).isEqualTo(info.width); + assertThat(hardwareBuffer.getHeight()).isEqualTo(info.height); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGBA_8888); + hardwareBuffer.close(); + + for (float scaleFactor : SCALE_FACTORS) { + int targetWidth = (int) (info.width * scaleFactor); + int targetHeight = (int) (info.height * scaleFactor); + hardwareBuffer = + AvifDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), targetWidth, targetHeight, image.threads); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getWidth()).isEqualTo(targetWidth); + assertThat(hardwareBuffer.getHeight()).isEqualTo(targetHeight); + Bitmap hardwareBitmap = + Bitmap.wrapHardwareBuffer(hardwareBuffer, ColorSpace.get(ColorSpace.Named.SRGB)); + assertThat(hardwareBitmap).isNotNull(); + assertThat(hardwareBitmap.getWidth()).isEqualTo(targetWidth); + assertThat(hardwareBitmap.getHeight()).isEqualTo(targetHeight); + hardwareBitmap.recycle(); + hardwareBuffer.close(); + } + } + + @Test + public void testDecodeToHardwareBufferRegularClass() throws IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return; + } + if (config != Config.ARGB_8888) { + return; + } + ByteBuffer buffer = image.getBuffer(); + assertThat(buffer).isNotNull(); + AvifDecoder decoder = AvifDecoder.create(buffer, image.threads); + assertThat(decoder).isNotNull(); + for (int i = 0; i < image.frameCount; ++i) { + assertThat(decoder.nextFrameIndex()).isEqualTo(i); + HardwareBuffer hardwareBuffer = decoder.nextFrameHardwareBuffer(); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getWidth()).isEqualTo(image.width); + assertThat(hardwareBuffer.getHeight()).isEqualTo(image.height); + hardwareBuffer.close(); + } + if (image.isAnimated) { + HardwareBuffer hardwareBuffer = decoder.nthFrameHardwareBuffer(0); + assertThat(hardwareBuffer).isNotNull(); + hardwareBuffer.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..4bbf646f66 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,9 @@ package org.aomedia.avif.android; import android.graphics.Bitmap; +import android.hardware.HardwareBuffer; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import java.nio.ByteBuffer; /** @@ -16,8 +18,9 @@ * *

This class can be accessed statically without instantiating an object. This is useful to * simply sniff and decode still AVIF images without having to maintain any decoder state. The - * following are the methods that can be accessed this way: {@link isAvifImage}, {@link getInfo} and - * {@link decode}. The {@link Info} inner class is used only in this case. + * following are the methods that can be accessed this way: {@link isAvifImage}, {@link getInfo}, + * {@link decode} and {@link decodeToHardwareBuffer}. The {@link Info} inner class is used only in + * this case. * *

2) As an instantiated regular class. * @@ -118,6 +121,44 @@ public static boolean decode(ByteBuffer encoded, int length, Bitmap bitmap) { */ public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads); + /** + * Decodes the AVIF image into an {@link HardwareBuffer} with RGBA_8888 pixels. + * + *

The returned buffer can be wrapped as a hardware {@link Bitmap} via {@link + * Bitmap#wrapHardwareBuffer(HardwareBuffer, android.graphics.ColorSpace)} on API 29+. Callers on + * older API levels must not use this method. + * + *

If {@code targetWidth} and {@code targetHeight} are both positive, the decoded image is + * scaled to those dimensions before RGB conversion. Otherwise the cropped image dimensions are + * used. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. + * @param length Length of the encoded buffer. + * @param targetWidth Desired output width, or 0 to use the image width. + * @param targetHeight Desired output height, or 0 to use the image height. + * @param threads Number of threads to be used for the AVIF decode. + * @return a HardwareBuffer on success, or null on failure. + */ + @RequiresApi(29) + public static HardwareBuffer decodeToHardwareBuffer( + ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads) { + return decodeToHardwareBufferNative(encoded, length, targetWidth, targetHeight, threads); + } + + /** + * Decodes the AVIF image into an {@link HardwareBuffer} at the cropped image dimensions. + * + * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) + */ + @RequiresApi(29) + public static HardwareBuffer decodeToHardwareBuffer( + ByteBuffer encoded, int length, int threads) { + return decodeToHardwareBuffer(encoded, length, 0, 0, threads); + } + + private static native HardwareBuffer decodeToHardwareBufferNative( + ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads); + /** Get the width of the image. */ public int getWidth() { return width; @@ -208,6 +249,33 @@ public int nextFrame(Bitmap bitmap) { private native int nextFrame(long decoder, Bitmap bitmap); + /** + * Decodes the next frame of the animated AVIF into an {@link HardwareBuffer}. + * + * @param targetWidth Desired output width, or 0 to use the image width. + * @param targetHeight Desired output height, or 0 to use the image height. + * @return a HardwareBuffer on success, or null on failure. + * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) + */ + @RequiresApi(29) + @Nullable + public HardwareBuffer nextFrameHardwareBuffer(int targetWidth, int targetHeight) { + return nextFrameHardwareBuffer(decoder, targetWidth, targetHeight); + } + + /** + * Decodes the next frame of the animated AVIF into an {@link HardwareBuffer} at the cropped + * image dimensions. + */ + @RequiresApi(29) + @Nullable + public HardwareBuffer nextFrameHardwareBuffer() { + return nextFrameHardwareBuffer(decoder, 0, 0); + } + + private native HardwareBuffer nextFrameHardwareBuffer( + long decoder, int targetWidth, int targetHeight); + /** * Get the 0-based index of the frame that will be returned by the next call to {@link nextFrame}. * If the returned value is same as {@link getFrameCount}, then the next call to {@link nextFrame} @@ -238,6 +306,34 @@ public int nthFrame(int n, Bitmap bitmap) { private native int nthFrame(long decoder, int n, Bitmap bitmap); + /** + * Decodes the nth frame of the animated AVIF into an {@link HardwareBuffer}. + * + * @param n The zero-based index of the frame to be decoded. + * @param targetWidth Desired output width, or 0 to use the image width. + * @param targetHeight Desired output height, or 0 to use the image height. + * @return a HardwareBuffer on success, or null on failure. + * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) + */ + @RequiresApi(29) + @Nullable + public HardwareBuffer nthFrameHardwareBuffer(int n, int targetWidth, int targetHeight) { + return nthFrameHardwareBuffer(decoder, n, targetWidth, targetHeight); + } + + /** + * Decodes the nth frame of the animated AVIF into an {@link HardwareBuffer} at the cropped + * image dimensions. + */ + @RequiresApi(29) + @Nullable + public HardwareBuffer nthFrameHardwareBuffer(int n) { + return nthFrameHardwareBuffer(decoder, n, 0, 0); + } + + private native HardwareBuffer nthFrameHardwareBuffer( + long decoder, int n, int targetWidth, int targetHeight); + /** * Returns a String describing an avifResult enum value. * diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt index 186831772c..aef583efff 100644 --- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt +++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt @@ -44,4 +44,4 @@ 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) +target_link_libraries(avif_android jnigraphics nativewindow 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 cec1c880e5..e17cff5a96 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 #include +#include +#include #include #include #include @@ -81,6 +84,31 @@ bool ValidateDirectBuffer(JNIEnv* env, jobject encoded, jint length, return true; } +int getThreadCount(int threads) { + if (threads < 0) { + return android_getCpuCount(); + } + if (threads == 0) { + // Empirically, on Android devices with more than 1 core, decoding with 2 + // threads is almost always better than using as many threads as CPU cores. + return std::min(android_getCpuCount(), 2); + } + return threads; +} + +// Checks if there is a pending JNI exception that will be thrown when the +// control returns to the java layer. If there is none, it will return false. If +// there is one, then it will clear the pending exception and return true. +// Whenever this function returns true, the caller should treat it as a fatal +// error and return with a failure status as early as possible. +bool JniExceptionCheck(JNIEnv* env) { + if (!env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, const uint8_t* const buffer, size_t length, int threads) { @@ -133,31 +161,26 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, return true; } -avifResult AvifImageToBitmap(JNIEnv* const env, - AvifDecoderWrapper* const decoder, - jobject bitmap) { - AndroidBitmapInfo bitmap_info; - if (AndroidBitmap_getInfo(env, bitmap, &bitmap_info) < 0) { - LOGE("AndroidBitmap_getInfo failed."); - return AVIF_RESULT_UNKNOWN_ERROR; - } - // Ensure that the bitmap format is RGBA_8888, RGB_565 or RGBA_F16. - if (bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 && - bitmap_info.format != ANDROID_BITMAP_FORMAT_RGB_565 && - bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_F16) { - LOGE("Bitmap format (%d) is not supported.", bitmap_info.format); - return AVIF_RESULT_NOT_IMPLEMENTED; - } - void* bitmap_pixels = nullptr; - if (AndroidBitmap_lockPixels(env, bitmap, &bitmap_pixels) != - ANDROID_BITMAP_RESULT_SUCCESS) { - LOGE("Failed to lock Bitmap."); - return AVIF_RESULT_UNKNOWN_ERROR; +void GetTargetDimensions(AvifDecoderWrapper* const decoder, int target_width, + int target_height, uint32_t* dst_width, + uint32_t* dst_height) { + if (target_width > 0 && target_height > 0) { + *dst_width = static_cast(target_width); + *dst_height = static_cast(target_height); + } else { + *dst_width = decoder->crop.width; + *dst_height = decoder->crop.height; } +} + +avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, + uint32_t dst_width, uint32_t dst_height, + avifResult* res, + std::unique_ptr& + cropped_image, + std::unique_ptr& + image_copy) { avifImage* image; - std::unique_ptr cropped_image( - nullptr, avifImageDestroy); - avifResult res; if (decoder->decoder->image->width == decoder->crop.width && decoder->decoder->image->height == decoder->crop.height && decoder->crop.x == 0 && decoder->crop.y == 0) { @@ -166,67 +189,219 @@ avifResult AvifImageToBitmap(JNIEnv* const env, cropped_image.reset(avifImageCreateEmpty()); if (cropped_image == nullptr) { LOGE("Failed to allocate cropped image."); - return AVIF_RESULT_OUT_OF_MEMORY; + *res = AVIF_RESULT_OUT_OF_MEMORY; + return nullptr; } - res = avifImageSetViewRect(cropped_image.get(), decoder->decoder->image, - &decoder->crop); - if (res != AVIF_RESULT_OK) { - LOGE("Failed to set crop rectangle. Status: %d", res); - return res; + *res = avifImageSetViewRect(cropped_image.get(), decoder->decoder->image, + &decoder->crop); + if (*res != AVIF_RESULT_OK) { + LOGE("Failed to set crop rectangle. Status: %d", *res); + return nullptr; } image = cropped_image.get(); } - std::unique_ptr image_copy( - nullptr, avifImageDestroy); - if (image->width != bitmap_info.width || - image->height != bitmap_info.height) { - // If the avifImage does not own the planes, then create a copy for safe - // scaling. + if (image->width != dst_width || image->height != dst_height) { if (!image->imageOwnsYUVPlanes || !image->imageOwnsAlphaPlane) { image_copy.reset(avifImageCreateEmpty()); if (image_copy == nullptr) { LOGE("Failed to allocate image for scaling."); - return AVIF_RESULT_OUT_OF_MEMORY; + *res = AVIF_RESULT_OUT_OF_MEMORY; + return nullptr; } - res = avifImageCopy(image_copy.get(), image, AVIF_PLANES_ALL); - if (res != AVIF_RESULT_OK) { - LOGE("Failed to make a copy of the image for scaling. Status: %d", res); - return res; + *res = avifImageCopy(image_copy.get(), image, AVIF_PLANES_ALL); + if (*res != AVIF_RESULT_OK) { + LOGE("Failed to make a copy of the image for scaling. Status: %d", *res); + return nullptr; } image = image_copy.get(); } avifDiagnostics diag; - res = avifImageScale(image, bitmap_info.width, bitmap_info.height, &diag); - if (res != AVIF_RESULT_OK) { - LOGE("Failed to scale image. Status: %d", res); - return res; + *res = avifImageScale(image, dst_width, dst_height, &diag); + if (*res != AVIF_RESULT_OK) { + LOGE("Failed to scale image. Status: %d", *res); + return nullptr; } } + *res = AVIF_RESULT_OK; + return image; +} + +avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, + uint32_t dst_width, uint32_t dst_height, + size_t row_bytes, avifRGBFormat format, + int depth, avifBool is_float) { + avifResult res; + std::unique_ptr cropped_image( + nullptr, avifImageDestroy); + std::unique_ptr image_copy( + nullptr, avifImageDestroy); + avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, + cropped_image, image_copy); + if (image == nullptr) { + return res; + } avifRGBImage rgb_image; avifRGBImageSetDefaults(&rgb_image, image); - if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGBA_F16) { - rgb_image.depth = 16; - rgb_image.isFloat = AVIF_TRUE; - } else if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGB_565) { - rgb_image.format = AVIF_RGB_FORMAT_RGB_565; - rgb_image.depth = 8; - } else { - rgb_image.depth = 8; - } - rgb_image.pixels = static_cast(bitmap_pixels); - rgb_image.rowBytes = bitmap_info.stride; + rgb_image.format = format; + rgb_image.depth = depth; + rgb_image.isFloat = is_float; + rgb_image.pixels = static_cast(pixels); + rgb_image.rowBytes = row_bytes; // Android always sees the Bitmaps as premultiplied with alpha when it renders // them: // https://developer.android.com/reference/android/graphics/Bitmap#setPremultiplied(boolean) rgb_image.alphaPremultiplied = AVIF_TRUE; res = avifImageYUVToRGB(image, &rgb_image); - AndroidBitmap_unlockPixels(env, bitmap); if (res != AVIF_RESULT_OK) { LOGE("Failed to convert YUV Pixels to RGB. Status: %d", res); - return res; } - return AVIF_RESULT_OK; + return res; +} + +avifResult AvifImageToBitmap(JNIEnv* const env, + AvifDecoderWrapper* const decoder, + jobject bitmap) { + AndroidBitmapInfo bitmap_info; + if (AndroidBitmap_getInfo(env, bitmap, &bitmap_info) < 0) { + LOGE("AndroidBitmap_getInfo failed."); + return AVIF_RESULT_UNKNOWN_ERROR; + } + // Ensure that the bitmap format is RGBA_8888, RGB_565 or RGBA_F16. + if (bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 && + bitmap_info.format != ANDROID_BITMAP_FORMAT_RGB_565 && + bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_F16) { + LOGE("Bitmap format (%d) is not supported.", bitmap_info.format); + return AVIF_RESULT_NOT_IMPLEMENTED; + } + void* bitmap_pixels = nullptr; + if (AndroidBitmap_lockPixels(env, bitmap, &bitmap_pixels) != + ANDROID_BITMAP_RESULT_SUCCESS) { + LOGE("Failed to lock Bitmap."); + return AVIF_RESULT_UNKNOWN_ERROR; + } + + avifRGBFormat format = AVIF_RGB_FORMAT_RGBA; + int depth = 8; + avifBool is_float = AVIF_FALSE; + if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGBA_F16) { + depth = 16; + is_float = AVIF_TRUE; + } else if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGB_565) { + format = AVIF_RGB_FORMAT_RGB_565; + } + + const avifResult res = AvifImageToRGBBuffer( + decoder, bitmap_pixels, bitmap_info.width, bitmap_info.height, + bitmap_info.stride, format, depth, is_float); + AndroidBitmap_unlockPixels(env, bitmap); + return res; +} + +jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, + AvifDecoderWrapper* const decoder, + uint32_t dst_width, uint32_t dst_height) { + if (android_get_device_api_level() < 29) { + LOGE("HardwareBuffer decode requires API 29+."); + return nullptr; + } + + AHardwareBuffer_Desc desc = {}; + desc.width = dst_width; + desc.height = dst_height; + desc.layers = 1; + desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; + + AHardwareBuffer* hw_buffer = nullptr; + if (AHardwareBuffer_allocate(&desc, &hw_buffer) != 0) { + LOGE("AHardwareBuffer_allocate failed."); + return nullptr; + } + AHardwareBuffer_describe(hw_buffer, &desc); + + void* pixels = nullptr; + if (AHardwareBuffer_lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, + -1, nullptr, &pixels) != 0) { + LOGE("AHardwareBuffer_lock failed."); + AHardwareBuffer_release(hw_buffer); + return nullptr; + } + + const avifResult res = AvifImageToRGBBuffer( + decoder, pixels, dst_width, dst_height, + static_cast(desc.stride) * 4, AVIF_RGB_FORMAT_RGBA, 8, + AVIF_FALSE); + AHardwareBuffer_unlock(hw_buffer, nullptr); + if (res != AVIF_RESULT_OK) { + AHardwareBuffer_release(hw_buffer); + return nullptr; + } + + jobject java_buffer = AHardwareBuffer_toHardwareBuffer(env, hw_buffer); + AHardwareBuffer_release(hw_buffer); + if (java_buffer == nullptr) { + LOGE("AHardwareBuffer_toHardwareBuffer failed."); + if (JniExceptionCheck(env)) { + return nullptr; + } + } + return java_buffer; +} + +jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, + int target_width, int target_height, + int threads) { + const uint8_t* buffer = nullptr; + size_t size = 0; + if (!ValidateDirectBuffer(env, encoded, length, &buffer, &size)) { + return nullptr; + } + AvifDecoderWrapper decoder; + if (!CreateDecoderAndParse(&decoder, buffer, size, + getThreadCount(threads))) { + return nullptr; + } + uint32_t dst_width = 0; + uint32_t dst_height = 0; + GetTargetDimensions(&decoder, target_width, target_height, &dst_width, + &dst_height); + if (avifDecoderNextImage(decoder.decoder) != AVIF_RESULT_OK) { + LOGE("Failed to decode AVIF image for HardwareBuffer output."); + return nullptr; + } + return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height); +} + +jobject NextFrameToHardwareBuffer(JNIEnv* const env, + AvifDecoderWrapper* const decoder, + int target_width, int target_height) { + const avifResult decode_result = avifDecoderNextImage(decoder->decoder); + if (decode_result != AVIF_RESULT_OK) { + LOGE("Failed to decode AVIF image. Status: %d", decode_result); + return nullptr; + } + uint32_t dst_width = 0; + uint32_t dst_height = 0; + GetTargetDimensions(decoder, target_width, target_height, &dst_width, + &dst_height); + return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); +} + +jobject NthFrameToHardwareBuffer(JNIEnv* const env, + AvifDecoderWrapper* const decoder, uint32_t n, + int target_width, int target_height) { + const avifResult decode_result = avifDecoderNthImage(decoder->decoder, n); + if (decode_result != AVIF_RESULT_OK) { + LOGE("Failed to decode AVIF image. Status: %d", decode_result); + return nullptr; + } + uint32_t dst_width = 0; + uint32_t dst_height = 0; + GetTargetDimensions(decoder, target_width, target_height, &dst_width, + &dst_height); + return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); } avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, @@ -249,31 +424,6 @@ avifResult DecodeNthImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, return AvifImageToBitmap(env, decoder, bitmap); } -int getThreadCount(int threads) { - if (threads < 0) { - return android_getCpuCount(); - } - if (threads == 0) { - // Empirically, on Android devices with more than 1 core, decoding with 2 - // threads is almost always better than using as many threads as CPU cores. - return std::min(android_getCpuCount(), 2); - } - return threads; -} - -// Checks if there is a pending JNI exception that will be thrown when the -// control returns to the java layer. If there is none, it will return false. If -// there is one, then it will clear the pending exception and return true. -// Whenever this function returns true, the caller should treat it as a fatal -// error and return with a failure status as early as possible. -bool JniExceptionCheck(JNIEnv* env) { - if (!env->ExceptionCheck()) { - return false; - } - env->ExceptionClear(); - return true; -} - } // namespace jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { @@ -353,6 +503,13 @@ FUNC(jboolean, decode, jobject encoded, int length, jobject bitmap, return DecodeNextImage(env, &decoder, bitmap) == AVIF_RESULT_OK; } +FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, + jint target_width, jint target_height, jint threads) { + IGNORE_UNUSED_JNI_PARAMETERS; + return DecodeToHardwareBuffer(env, encoded, length, target_width, + target_height, threads); +} + FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) { const uint8_t* buffer = nullptr; size_t size = 0; @@ -444,6 +601,23 @@ FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { return DecodeNthImage(env, decoder, n, bitmap); } +FUNC(jobject, nextFrameHardwareBuffer, jlong jdecoder, jint target_width, + jint target_height) { + IGNORE_UNUSED_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + return NextFrameToHardwareBuffer(env, decoder, target_width, target_height); +} + +FUNC(jobject, nthFrameHardwareBuffer, jlong jdecoder, jint n, + jint target_width, jint target_height) { + IGNORE_UNUSED_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + return NthFrameToHardwareBuffer(env, decoder, static_cast(n), + target_width, target_height); +} + FUNC(jstring, resultToString, jint result) { IGNORE_UNUSED_JNI_PARAMETERS; return env->NewStringUTF(avifResultToString(static_cast(result))); diff --git a/cmake/Modules/LocalDav1d.cmake b/cmake/Modules/LocalDav1d.cmake index b03bab8312..58b1549716 100644 --- a/cmake/Modules/LocalDav1d.cmake +++ b/cmake/Modules/LocalDav1d.cmake @@ -74,6 +74,10 @@ function(avif_build_local_dav1d) endif() file(MAKE_DIRECTORY ${install_dir}/include) + if(ANDROID) + set(DAV1D_BITDEPTHS_ARG -Dbitdepths=8) + endif() + ExternalProject_Add( dav1d ${download_step_args} @@ -89,7 +93,7 @@ function(avif_build_local_dav1d) CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env "PATH=${PATH}" ${MESON_EXECUTABLE} setup --buildtype=release --default-library=static --prefix= --libdir=lib -Denable_asm=true -Denable_tools=false -Denable_examples=false - -Denable_tests=false ${EXTRA_ARGS} + -Denable_tests=false ${DAV1D_BITDEPTHS_ARG} ${EXTRA_ARGS} BUILD_COMMAND ${CMAKE_COMMAND} -E env "PATH=${PATH}" ${NINJA_EXECUTABLE} -C INSTALL_COMMAND ${CMAKE_COMMAND} -E env "PATH=${PATH}" ${NINJA_EXECUTABLE} -C install BUILD_BYPRODUCTS /lib/libdav1d.a diff --git a/ext/dav1d_android.sh b/ext/dav1d_android.sh index 2000a4ba98..d31faa2446 100755 --- a/ext/dav1d_android.sh +++ b/ext/dav1d_android.sh @@ -4,6 +4,9 @@ # This script only works on linux. You must pass the path to the android NDK as # a parameter to this script. # +# The build is configured for 8-bit AV1 only (-Dbitdepths=8) to reduce binary +# size. 10/12-bit AVIF images will fail to decode with this configuration. +# # Android NDK: https://developer.android.com/ndk/downloads # # The git tag below is known to work, and will occasionally be updated. Feel @@ -32,6 +35,6 @@ for i in "${!ABI_LIST[@]}"; do abi="${ABI_LIST[i]}" PATH=$PATH:${android_bin} meson setup --default-library=static --buildtype release \ --cross-file="../../package/crossfiles/${ARCH_LIST[i]}-android.meson" \ - -Denable_tools=false -Denable_tests=false "dav1d/build/${abi}" dav1d + -Dbitdepths=8 -Denable_tools=false -Denable_tests=false "dav1d/build/${abi}" dav1d PATH=$PATH:${android_bin} meson compile -C "dav1d/build/${abi}" done From b22a1bf75289b85247d151c0206a261fb28366a3 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 11 Jul 2026 23:26:09 +0900 Subject: [PATCH 02/25] =?UTF-8?q?=E9=81=8E=E5=8E=BBVer=E3=82=92=E8=80=83?= =?UTF-8?q?=E6=85=AE=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/avifandroidjni/proguard-rules.pro | 3 + .../aomedia/avif/android/AvifDecoderTest.java | 10 +- .../org/aomedia/avif/android/AvifDecoder.java | 103 ++--------- .../avif/android/AvifHardwareDecoder.java | 109 ++++++++++++ .../src/main/jni/CMakeLists.txt | 2 +- .../src/main/jni/libavif_jni.cc | 168 +++++++++++++++--- 6 files changed, 270 insertions(+), 125 deletions(-) create mode 100644 android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java diff --git a/android_jni/avifandroidjni/proguard-rules.pro b/android_jni/avifandroidjni/proguard-rules.pro index 180acdaf2a..a3052713eb 100644 --- a/android_jni/avifandroidjni/proguard-rules.pro +++ b/android_jni/avifandroidjni/proguard-rules.pro @@ -15,3 +15,6 @@ -keep class org.aomedia.avif.android.AvifDecoder$Info { *; } +-keep class org.aomedia.avif.android.AvifHardwareDecoder { + *; +} 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 fabc835026..1cb1ec6f87 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 @@ -273,7 +273,7 @@ public void testDecodeToHardwareBuffer() throws IOException { assertThat(AvifDecoder.getInfo(buffer, buffer.remaining(), info)).isTrue(); HardwareBuffer hardwareBuffer = - AvifDecoder.decodeToHardwareBuffer(buffer, buffer.remaining(), image.threads); + AvifHardwareDecoder.decodeToHardwareBuffer(buffer, buffer.remaining(), image.threads); assertThat(hardwareBuffer).isNotNull(); assertThat(hardwareBuffer.getWidth()).isEqualTo(info.width); assertThat(hardwareBuffer.getHeight()).isEqualTo(info.height); @@ -284,7 +284,7 @@ public void testDecodeToHardwareBuffer() throws IOException { int targetWidth = (int) (info.width * scaleFactor); int targetHeight = (int) (info.height * scaleFactor); hardwareBuffer = - AvifDecoder.decodeToHardwareBuffer( + AvifHardwareDecoder.decodeToHardwareBuffer( buffer, buffer.remaining(), targetWidth, targetHeight, image.threads); assertThat(hardwareBuffer).isNotNull(); assertThat(hardwareBuffer.getWidth()).isEqualTo(targetWidth); @@ -313,14 +313,16 @@ public void testDecodeToHardwareBufferRegularClass() throws IOException { assertThat(decoder).isNotNull(); for (int i = 0; i < image.frameCount; ++i) { assertThat(decoder.nextFrameIndex()).isEqualTo(i); - HardwareBuffer hardwareBuffer = decoder.nextFrameHardwareBuffer(); + HardwareBuffer hardwareBuffer = AvifHardwareDecoder.nextFrameHardwareBuffer( + decoder.getNativeDecoderHandle()); assertThat(hardwareBuffer).isNotNull(); assertThat(hardwareBuffer.getWidth()).isEqualTo(image.width); assertThat(hardwareBuffer.getHeight()).isEqualTo(image.height); hardwareBuffer.close(); } if (image.isAnimated) { - HardwareBuffer hardwareBuffer = decoder.nthFrameHardwareBuffer(0); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.nthFrameHardwareBuffer(decoder.getNativeDecoderHandle(), 0); assertThat(hardwareBuffer).isNotNull(); hardwareBuffer.close(); } 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 4bbf646f66..5682b6d751 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,9 +4,7 @@ package org.aomedia.avif.android; import android.graphics.Bitmap; -import android.hardware.HardwareBuffer; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; import java.nio.ByteBuffer; /** @@ -18,9 +16,11 @@ * *

This class can be accessed statically without instantiating an object. This is useful to * simply sniff and decode still AVIF images without having to maintain any decoder state. The - * following are the methods that can be accessed this way: {@link isAvifImage}, {@link getInfo}, - * {@link decode} and {@link decodeToHardwareBuffer}. The {@link Info} inner class is used only in - * this case. + * following are the methods that can be accessed this way: {@link isAvifImage}, {@link getInfo} and + * {@link decode}. The {@link Info} inner class is used only in this case. + * + *

For direct {@link android.hardware.HardwareBuffer} output on API 29+, use {@link + * AvifHardwareDecoder} instead. * *

2) As an instantiated regular class. * @@ -122,43 +122,15 @@ public static boolean decode(ByteBuffer encoded, int length, Bitmap bitmap) { public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads); /** - * Decodes the AVIF image into an {@link HardwareBuffer} with RGBA_8888 pixels. - * - *

The returned buffer can be wrapped as a hardware {@link Bitmap} via {@link - * Bitmap#wrapHardwareBuffer(HardwareBuffer, android.graphics.ColorSpace)} on API 29+. Callers on - * older API levels must not use this method. - * - *

If {@code targetWidth} and {@code targetHeight} are both positive, the decoded image is - * scaled to those dimensions before RGB conversion. Otherwise the cropped image dimensions are - * used. - * - * @param encoded The encoded AVIF image. encoded.position() must be 0. - * @param length Length of the encoded buffer. - * @param targetWidth Desired output width, or 0 to use the image width. - * @param targetHeight Desired output height, or 0 to use the image height. - * @param threads Number of threads to be used for the AVIF decode. - * @return a HardwareBuffer on success, or null on failure. - */ - @RequiresApi(29) - public static HardwareBuffer decodeToHardwareBuffer( - ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads) { - return decodeToHardwareBufferNative(encoded, length, targetWidth, targetHeight, threads); - } - - /** - * Decodes the AVIF image into an {@link HardwareBuffer} at the cropped image dimensions. + * Returns the native decoder handle used by {@link AvifHardwareDecoder} on API 29+. * - * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) + *

Do not call {@link #release()} until all hardware-buffer decodes using this handle have + * finished. */ - @RequiresApi(29) - public static HardwareBuffer decodeToHardwareBuffer( - ByteBuffer encoded, int length, int threads) { - return decodeToHardwareBuffer(encoded, length, 0, 0, threads); + long getNativeDecoderHandle() { + return decoder; } - private static native HardwareBuffer decodeToHardwareBufferNative( - ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads); - /** Get the width of the image. */ public int getWidth() { return width; @@ -249,33 +221,6 @@ public int nextFrame(Bitmap bitmap) { private native int nextFrame(long decoder, Bitmap bitmap); - /** - * Decodes the next frame of the animated AVIF into an {@link HardwareBuffer}. - * - * @param targetWidth Desired output width, or 0 to use the image width. - * @param targetHeight Desired output height, or 0 to use the image height. - * @return a HardwareBuffer on success, or null on failure. - * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) - */ - @RequiresApi(29) - @Nullable - public HardwareBuffer nextFrameHardwareBuffer(int targetWidth, int targetHeight) { - return nextFrameHardwareBuffer(decoder, targetWidth, targetHeight); - } - - /** - * Decodes the next frame of the animated AVIF into an {@link HardwareBuffer} at the cropped - * image dimensions. - */ - @RequiresApi(29) - @Nullable - public HardwareBuffer nextFrameHardwareBuffer() { - return nextFrameHardwareBuffer(decoder, 0, 0); - } - - private native HardwareBuffer nextFrameHardwareBuffer( - long decoder, int targetWidth, int targetHeight); - /** * Get the 0-based index of the frame that will be returned by the next call to {@link nextFrame}. * If the returned value is same as {@link getFrameCount}, then the next call to {@link nextFrame} @@ -306,34 +251,6 @@ public int nthFrame(int n, Bitmap bitmap) { private native int nthFrame(long decoder, int n, Bitmap bitmap); - /** - * Decodes the nth frame of the animated AVIF into an {@link HardwareBuffer}. - * - * @param n The zero-based index of the frame to be decoded. - * @param targetWidth Desired output width, or 0 to use the image width. - * @param targetHeight Desired output height, or 0 to use the image height. - * @return a HardwareBuffer on success, or null on failure. - * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) - */ - @RequiresApi(29) - @Nullable - public HardwareBuffer nthFrameHardwareBuffer(int n, int targetWidth, int targetHeight) { - return nthFrameHardwareBuffer(decoder, n, targetWidth, targetHeight); - } - - /** - * Decodes the nth frame of the animated AVIF into an {@link HardwareBuffer} at the cropped - * image dimensions. - */ - @RequiresApi(29) - @Nullable - public HardwareBuffer nthFrameHardwareBuffer(int n) { - return nthFrameHardwareBuffer(decoder, n, 0, 0); - } - - private native HardwareBuffer nthFrameHardwareBuffer( - long decoder, int n, int targetWidth, int targetHeight); - /** * Returns a String describing an avifResult enum value. * diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java new file mode 100644 index 0000000000..ede8ccaec5 --- /dev/null +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -0,0 +1,109 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +package org.aomedia.avif.android; + +import android.hardware.HardwareBuffer; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import java.nio.ByteBuffer; + +/** + * AVIF decoder entry points that write directly into {@link HardwareBuffer} objects. + * + *

This class is separate from {@link AvifDecoder} so that apps with {@code minSdkVersion} below + * 26 can still load and use {@link AvifDecoder} on older devices. Only call into this class on API + * 29+ after checking {@code Build.VERSION.SDK_INT}. + */ +@RequiresApi(29) +public final class AvifHardwareDecoder { + private AvifHardwareDecoder() {} + + static { + try { + System.loadLibrary("avif_android"); + } catch (UnsatisfiedLinkError exception) { + exception.printStackTrace(); + } + } + + /** + * Decodes the AVIF image into an {@link HardwareBuffer} with RGBA_8888 pixels. + * + *

The returned buffer can be wrapped as a hardware {@link android.graphics.Bitmap} via {@link + * android.graphics.Bitmap#wrapHardwareBuffer(HardwareBuffer, + * android.graphics.ColorSpace)}. + * + *

If {@code targetWidth} and {@code targetHeight} are both positive, the decoded image is + * scaled to those dimensions before RGB conversion. Otherwise the cropped image dimensions are + * used. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. + * @param length Length of the encoded buffer. + * @param targetWidth Desired output width, or 0 to use the image width. + * @param targetHeight Desired output height, or 0 to use the image height. + * @param threads Number of threads to be used for the AVIF decode. + * @return a HardwareBuffer on success, or null on failure. + */ + @Nullable + public static HardwareBuffer decodeToHardwareBuffer( + ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads) { + return decodeToHardwareBufferNative(encoded, length, targetWidth, targetHeight, threads); + } + + /** + * Decodes the AVIF image into an {@link HardwareBuffer} at the cropped image dimensions. + * + * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) + */ + @Nullable + public static HardwareBuffer decodeToHardwareBuffer(ByteBuffer encoded, int length, int threads) { + return decodeToHardwareBuffer(encoded, length, 0, 0, threads); + } + + /** + * Decodes the next frame of an animated AVIF into an {@link HardwareBuffer}. + * + * @param nativeDecoderHandle Native decoder handle obtained from {@link + * AvifDecoder#getNativeDecoderHandle()}. + */ + @Nullable + public static HardwareBuffer nextFrameHardwareBuffer( + long nativeDecoderHandle, int targetWidth, int targetHeight) { + return nextFrameHardwareBufferNative(nativeDecoderHandle, targetWidth, targetHeight); + } + + /** Decodes the next frame at the cropped image dimensions. */ + @Nullable + public static HardwareBuffer nextFrameHardwareBuffer(long nativeDecoderHandle) { + return nextFrameHardwareBuffer(nativeDecoderHandle, 0, 0); + } + + /** + * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. + * + * @param nativeDecoderHandle Native decoder handle obtained from {@link + * AvifDecoder#getNativeDecoderHandle()}. + * @param n The zero-based index of the frame to be decoded. + */ + @Nullable + public static HardwareBuffer nthFrameHardwareBuffer( + long nativeDecoderHandle, int n, int targetWidth, int targetHeight) { + return nthFrameHardwareBufferNative(nativeDecoderHandle, n, targetWidth, targetHeight); + } + + /** Decodes the nth frame at the cropped image dimensions. */ + @Nullable + public static HardwareBuffer nthFrameHardwareBuffer(long nativeDecoderHandle, int n) { + return nthFrameHardwareBuffer(nativeDecoderHandle, n, 0, 0); + } + + private static native HardwareBuffer decodeToHardwareBufferNative( + ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads); + + private static native HardwareBuffer nextFrameHardwareBufferNative( + long nativeDecoderHandle, int targetWidth, int targetHeight); + + private static native HardwareBuffer nthFrameHardwareBufferNative( + long nativeDecoderHandle, int n, int targetWidth, int targetHeight); +} diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt index aef583efff..186831772c 100644 --- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt +++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt @@ -44,4 +44,4 @@ 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 nativewindow avif log cpufeatures) +target_link_libraries(avif_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 e17cff5a96..94756f017b 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -4,13 +4,15 @@ #include #include #include -#include #include #include +#include #include +#include #include #include +#include #include #include @@ -28,12 +30,125 @@ JNIEXPORT RETURN_TYPE Java_org_aomedia_avif_android_AvifDecoder_##NAME( \ JNIEnv* env, jobject thiz, ##__VA_ARGS__) +#define HW_FUNC(RETURN_TYPE, NAME, ...) \ + extern "C" { \ + JNIEXPORT RETURN_TYPE \ + Java_org_aomedia_avif_android_AvifHardwareDecoder_##NAME(JNIEnv* env, jclass clazz, \ + ##__VA_ARGS__); \ + } \ + JNIEXPORT RETURN_TYPE \ + Java_org_aomedia_avif_android_AvifHardwareDecoder_##NAME(JNIEnv* env, jclass clazz, \ + ##__VA_ARGS__) + #define IGNORE_UNUSED_JNI_PARAMETERS \ - (void) env; \ - (void) thiz + (void)env; \ + (void)thiz + +#define IGNORE_UNUSED_HW_JNI_PARAMETERS \ + (void)env; \ + (void)clazz namespace { +using AHardwareBuffer_allocate_fn = int (*)(const AHardwareBuffer_Desc*, AHardwareBuffer**); +using AHardwareBuffer_describe_fn = void (*)(const AHardwareBuffer*, AHardwareBuffer_Desc*); +using AHardwareBuffer_lock_fn = int (*)(AHardwareBuffer*, uint64_t, int32_t, const ARect*, + void**); +using AHardwareBuffer_unlock_fn = int (*)(AHardwareBuffer*, int32_t*); +using AHardwareBuffer_release_fn = void (*)(AHardwareBuffer*); +using AHardwareBuffer_toHardwareBuffer_fn = jobject (*)(JNIEnv*, AHardwareBuffer*); + +struct HardwareBufferApi { + void* android_library = nullptr; + void* nativewindow_library = nullptr; + AHardwareBuffer_allocate_fn allocate = nullptr; + AHardwareBuffer_describe_fn describe = nullptr; + AHardwareBuffer_lock_fn lock = nullptr; + AHardwareBuffer_unlock_fn unlock = nullptr; + AHardwareBuffer_release_fn release = nullptr; + AHardwareBuffer_toHardwareBuffer_fn to_hardware_buffer = nullptr; + bool init_attempted = false; + bool init_succeeded = false; +}; + +HardwareBufferApi g_hardware_buffer_api; + +int GetDeviceApiLevel() { + if (android_get_device_api_level != nullptr) { + const int api_level = android_get_device_api_level(); + if (api_level > 0) { + return api_level; + } + } + + char sdk_version[PROP_VALUE_MAX] = {}; + if (__system_property_get("ro.build.version.sdk", sdk_version) <= 0) { + return 0; + } + return atoi(sdk_version); +} + +bool EnsureHardwareBufferApiLoaded() { + if (g_hardware_buffer_api.init_attempted) { + return g_hardware_buffer_api.init_succeeded; + } + g_hardware_buffer_api.init_attempted = true; + + if (GetDeviceApiLevel() < 29) { + LOGE("HardwareBuffer decode requires API 29+."); + return false; + } + + g_hardware_buffer_api.android_library = dlopen("libandroid.so", RTLD_NOW); + if (g_hardware_buffer_api.android_library == nullptr) { + LOGE("Failed to dlopen libandroid.so: %s.", dlerror()); + return false; + } + + g_hardware_buffer_api.nativewindow_library = dlopen("libnativewindow.so", RTLD_NOW); + if (g_hardware_buffer_api.nativewindow_library == nullptr) { + LOGE("Failed to dlopen libnativewindow.so: %s.", dlerror()); + dlclose(g_hardware_buffer_api.android_library); + g_hardware_buffer_api.android_library = nullptr; + return false; + } + +#define LOAD_ANDROID_SYMBOL(name) \ + g_hardware_buffer_api.name = reinterpret_cast( \ + dlsym(g_hardware_buffer_api.android_library, "AHardwareBuffer_" #name)); \ + if (g_hardware_buffer_api.name == nullptr) { \ + LOGE("Failed to dlsym AHardwareBuffer_" #name ": %s.", dlerror()); \ + dlclose(g_hardware_buffer_api.nativewindow_library); \ + dlclose(g_hardware_buffer_api.android_library); \ + g_hardware_buffer_api.nativewindow_library = nullptr; \ + g_hardware_buffer_api.android_library = nullptr; \ + return false; \ + } + + LOAD_ANDROID_SYMBOL(allocate); + LOAD_ANDROID_SYMBOL(describe); + LOAD_ANDROID_SYMBOL(lock); + LOAD_ANDROID_SYMBOL(unlock); + LOAD_ANDROID_SYMBOL(release); + +#undef LOAD_ANDROID_SYMBOL + + g_hardware_buffer_api.to_hardware_buffer = + reinterpret_cast(dlsym( + g_hardware_buffer_api.nativewindow_library, "AHardwareBuffer_toHardwareBuffer")); + if (g_hardware_buffer_api.to_hardware_buffer == nullptr) { + LOGE("Failed to dlsym AHardwareBuffer_toHardwareBuffer: %s.", dlerror()); + dlclose(g_hardware_buffer_api.nativewindow_library); + dlclose(g_hardware_buffer_api.android_library); + g_hardware_buffer_api.nativewindow_library = nullptr; + g_hardware_buffer_api.android_library = nullptr; + return false; + } + + g_hardware_buffer_api.init_succeeded = true; + return true; +} + // RAII wrapper class that properly frees the decoder related objects on // destruction. struct AvifDecoderWrapper { @@ -301,8 +416,7 @@ avifResult AvifImageToBitmap(JNIEnv* const env, jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t dst_width, uint32_t dst_height) { - if (android_get_device_api_level() < 29) { - LOGE("HardwareBuffer decode requires API 29+."); + if (!EnsureHardwareBufferApiLoaded()) { return nullptr; } @@ -315,17 +429,17 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; AHardwareBuffer* hw_buffer = nullptr; - if (AHardwareBuffer_allocate(&desc, &hw_buffer) != 0) { + if (g_hardware_buffer_api.allocate(&desc, &hw_buffer) != 0) { LOGE("AHardwareBuffer_allocate failed."); return nullptr; } - AHardwareBuffer_describe(hw_buffer, &desc); + g_hardware_buffer_api.describe(hw_buffer, &desc); void* pixels = nullptr; - if (AHardwareBuffer_lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, - -1, nullptr, &pixels) != 0) { + if (g_hardware_buffer_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, + -1, nullptr, &pixels) != 0) { LOGE("AHardwareBuffer_lock failed."); - AHardwareBuffer_release(hw_buffer); + g_hardware_buffer_api.release(hw_buffer); return nullptr; } @@ -333,14 +447,14 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, decoder, pixels, dst_width, dst_height, static_cast(desc.stride) * 4, AVIF_RGB_FORMAT_RGBA, 8, AVIF_FALSE); - AHardwareBuffer_unlock(hw_buffer, nullptr); + g_hardware_buffer_api.unlock(hw_buffer, nullptr); if (res != AVIF_RESULT_OK) { - AHardwareBuffer_release(hw_buffer); + g_hardware_buffer_api.release(hw_buffer); return nullptr; } - jobject java_buffer = AHardwareBuffer_toHardwareBuffer(env, hw_buffer); - AHardwareBuffer_release(hw_buffer); + jobject java_buffer = g_hardware_buffer_api.to_hardware_buffer(env, hw_buffer); + g_hardware_buffer_api.release(hw_buffer); if (java_buffer == nullptr) { LOGE("AHardwareBuffer_toHardwareBuffer failed."); if (JniExceptionCheck(env)) { @@ -503,13 +617,6 @@ FUNC(jboolean, decode, jobject encoded, int length, jobject bitmap, return DecodeNextImage(env, &decoder, bitmap) == AVIF_RESULT_OK; } -FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, - jint target_width, jint target_height, jint threads) { - IGNORE_UNUSED_JNI_PARAMETERS; - return DecodeToHardwareBuffer(env, encoded, length, target_width, - target_height, threads); -} - FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) { const uint8_t* buffer = nullptr; size_t size = 0; @@ -601,17 +708,24 @@ FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { return DecodeNthImage(env, decoder, n, bitmap); } -FUNC(jobject, nextFrameHardwareBuffer, jlong jdecoder, jint target_width, - jint target_height) { - IGNORE_UNUSED_JNI_PARAMETERS; +HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, + jint target_width, jint target_height, jint threads) { + IGNORE_UNUSED_HW_JNI_PARAMETERS; + return DecodeToHardwareBuffer(env, encoded, length, target_width, + target_height, threads); +} + +HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_width, + jint target_height) { + IGNORE_UNUSED_HW_JNI_PARAMETERS; AvifDecoderWrapper* const decoder = reinterpret_cast(jdecoder); return NextFrameToHardwareBuffer(env, decoder, target_width, target_height); } -FUNC(jobject, nthFrameHardwareBuffer, jlong jdecoder, jint n, - jint target_width, jint target_height) { - IGNORE_UNUSED_JNI_PARAMETERS; +HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, + jint target_width, jint target_height) { + IGNORE_UNUSED_HW_JNI_PARAMETERS; AvifDecoderWrapper* const decoder = reinterpret_cast(jdecoder); return NthFrameToHardwareBuffer(env, decoder, static_cast(n), From a7cf17e09d5314d7aacfeb83b3f661146c8e42c6 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 11 Jul 2026 23:39:08 +0900 Subject: [PATCH 03/25] =?UTF-8?q?Animation=20Avif=E3=81=AE=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=AF=E4=B8=8D=E8=A6=81=E3=81=AA=E3=81=AE=E3=81=A7?= =?UTF-8?q?=E6=B6=88=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/avifandroidjni/proguard-rules.pro | 2 +- .../AvifDecoderLengthValidationTest.java | 42 +-- .../aomedia/avif/android/AvifDecoderTest.java | 154 +---------- .../org/aomedia/avif/android/AvifDecoder.java | 181 +------------ .../avif/android/AvifHardwareDecoder.java | 43 --- .../src/main/jni/libavif_jni.cc | 252 ++++-------------- 6 files changed, 60 insertions(+), 614 deletions(-) diff --git a/android_jni/avifandroidjni/proguard-rules.pro b/android_jni/avifandroidjni/proguard-rules.pro index a3052713eb..c6b330f6c6 100644 --- a/android_jni/avifandroidjni/proguard-rules.pro +++ b/android_jni/avifandroidjni/proguard-rules.pro @@ -10,7 +10,7 @@ # Members of these classes may be accessed from native methods. Keep them # unobfuscated. -keep class org.aomedia.avif.android.AvifDecoder { - *; + public *; } -keep class org.aomedia.avif.android.AvifDecoder$Info { *; diff --git a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java index 38f623b1d3..3f5f12e655 100644 --- a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java +++ b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java @@ -7,17 +7,14 @@ // getInfo(...) -> false // decode(...) -> false // isAvifImage(...) -> false -// create(...) -> null // // Happy-path cases in this file exist to guard against the new validation // accidentally over-rejecting legitimate inputs (length == capacity, -// length == 0, valid images through create()). +// length == 0). package org.aomedia.avif.android; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.content.res.AssetManager; @@ -265,43 +262,6 @@ public void isAvifImage_emptyDirectBuffer_returnsFalseNoCrash() { assertFalse(AvifDecoder.isAvifImage(buf)); } - // --------------------------------------------------------------------------- - // create (AvifDecoder factory). Uses encoded.remaining() internally, so the - // length branch can't be poisoned from the public Java surface. These tests - // guard against the createDecoder hardening accidentally breaking the happy - // path or the clean-failure contract on malformed input. - // --------------------------------------------------------------------------- - - @Test - public void create_truncatedFtypDirect_returnsNull() { - // The tiny_ftyp 8-byte payload is not a valid AVIF — create() must return - // null without crashing. - ByteBuffer buf = tinyFtypDirectBuffer(); - assertNull(AvifDecoder.create(buf)); - } - - @Test - public void create_heapBackedBuffer_returnsNull() { - // Non-direct buffer: GetDirectBufferCapacity returns -1 -> clean failure. - ByteBuffer buf = tinyFtypHeapBuffer(); - assertNull(AvifDecoder.create(buf)); - } - - @Test - public void create_emptyDirectBuffer_returnsNull() { - ByteBuffer buf = emptyDirectBuffer(); - assertNull(AvifDecoder.create(buf)); - } - - @Test - public void create_validImage_stillReturnsNonNull() throws IOException { - // PLAN.md §5 layer-2 case #8: guards against the private createDecoder - // hardening accidentally rejecting the happy path. - ByteBuffer buf = loadDirectAssetBuffer("avif/fox.profile0.8bpc.yuv420.avif"); - AvifDecoder decoder = AvifDecoder.create(buf); - assertNotNull(decoder); - } - @Test public void getInfo_validImageHonestLength_returnsTrue() throws IOException { // Sanity: after hardening, the honest-length path on a well-formed image 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 1cb1ec6f87..9bfd952d75 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 @@ -34,11 +34,7 @@ private static class Image { public final int height; public final int depth; public final boolean alphaPresent; - public final int frameCount; - public final int repetitionCount; - public final double frameDuration; public final int threads; - public final boolean isAnimated; public Image( String directory, @@ -48,41 +44,13 @@ public Image( int depth, boolean alphaPresent, int threads) { - this( - directory, - filename, - width, - height, - depth, - alphaPresent, - /* frameCount= */ 1, - /* repetitionCount= */ 0, - /* frameDuration= */ 0.0, - threads); - } - - public Image( - String directory, - String filename, - int width, - int height, - int depth, - boolean alphaPresent, - int frameCount, - int repetitionCount, - double frameDuration, - int threads) { this.directory = directory; this.filename = filename; this.width = width; this.height = height; this.depth = depth; this.alphaPresent = alphaPresent; - this.frameCount = frameCount; - this.repetitionCount = repetitionCount; - this.frameDuration = frameDuration; this.threads = threads; - this.isAnimated = frameCount > 1; } public ByteBuffer getBuffer() throws IOException { @@ -101,8 +69,7 @@ public ByteBuffer getBuffer() throws IOException { private static final float[] SCALE_FACTORS = {0.5f, 1.3f}; private static final Image[] IMAGES = { - // Parameter ordering for still images: directory, filename, width, height, depth, alphaPresent, - // threads. + // Parameter ordering: directory, filename, width, height, depth, alphaPresent, threads. new Image("avif", "fox.profile0.10bpc.yuv420.avif", 1204, 800, 10, false, 1), new Image("avif", "fox.profile0.10bpc.yuv420.monochrome.avif", 1204, 800, 10, false, 1), new Image("avif", "fox.profile0.8bpc.yuv420.avif", 1204, 800, 8, false, 1), @@ -116,11 +83,6 @@ public ByteBuffer getBuffer() throws IOException { new Image("avif", "fox.profile2.12bpc.yuv444.avif", 1204, 800, 12, false, 1), new Image("avif", "fox.profile2.8bpc.yuv422.avif", 1204, 800, 8, false, 1), new Image("avif", "blue-and-magenta-crop.avif", 180, 100, 8, true, 1), - // Parameter ordering for animated images: directory, filename, width, height, depth, - // alphaPresent, frameCount, repetitionCount, frameDuration, threads. - new Image("animated_avif", "alpha_video.avif", 640, 480, 8, true, 48, -2, 0.04, 1), - new Image( - "animated_avif", "Chimera-AV1-10bit-480x270.avif", 480, 270, 10, false, 95, -2, 0.04, 2), }; @Parameters @@ -129,9 +91,8 @@ public static List data() throws IOException { for (Image image : IMAGES) { // Test ARGB_8888 for all files. list.add(new Object[] {Config.ARGB_8888, image}); - // For 8bpc files and animated files, test only RGB_565 (F16 is flaky for animated files on - // x86 emulators). For other files, test only RGBA_F16. - Config testConfig = (image.depth == 8 || image.isAnimated) ? Config.RGB_565 : Config.RGBA_F16; + // For 8bpc files, test RGB_565. For other files, test RGBA_F16. + Config testConfig = image.depth == 8 ? Config.RGB_565 : Config.RGBA_F16; list.add(new Object[] {testConfig, image}); } return list; @@ -143,13 +104,8 @@ public static List data() throws IOException { @Parameter(1) public Image image; - // Tests AvifDecoder by using it as a utility class without instantiating it. Only still images - // can be decoded this way. @Test public void testDecodeUtilityClass() throws IOException { - if (image.isAnimated) { - return; - } ByteBuffer buffer = image.getBuffer(); assertThat(buffer).isNotNull(); assertThat(AvifDecoder.isAvifImage(buffer)).isTrue(); @@ -187,84 +143,12 @@ public void testDecodeUtilityClass() throws IOException { } } - // Tests AvifDecoder by using it as a regular instantiated class. - @Test - public void testDecodeRegularClass() throws IOException { - ByteBuffer buffer = image.getBuffer(); - assertThat(buffer).isNotNull(); - AvifDecoder decoder = AvifDecoder.create(buffer, image.threads); - assertThat(decoder).isNotNull(); - assertThat(decoder.getWidth()).isEqualTo(image.width); - assertThat(decoder.getHeight()).isEqualTo(image.height); - assertThat(decoder.getDepth()).isEqualTo(image.depth); - assertThat(decoder.getAlphaPresent()).isEqualTo(image.alphaPresent); - assertThat(decoder.getFrameCount()).isEqualTo(image.frameCount); - Bitmap bitmap = Bitmap.createBitmap(image.width, image.height, config); - assertThat(bitmap).isNotNull(); - for (int i = 0; i < image.frameCount; i++) { - assertThat(decoder.nextFrameIndex()).isEqualTo(i); - assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); - } - if (image.isAnimated) { - assertThat(decoder.getRepetitionCount()).isEqualTo(image.repetitionCount); - double[] frameDurations = decoder.getFrameDurations(); - assertThat(frameDurations).isNotNull(); - assertThat(frameDurations).hasLength(image.frameCount); - for (int i = 0; i < image.frameCount; i++) { - assertThat(frameDurations[i]).isWithin(1.0e-2).of(image.frameDuration); - } - assertThat(decoder.nextFrameIndex()).isEqualTo(image.frameCount); - // Fetch the first frame again. - assertThat(decoder.nthFrame(0, bitmap)).isEqualTo(AVIF_RESULT_OK); - // Now nextFrame will return the second frame. - assertThat(decoder.nextFrameIndex()).isEqualTo(1); - assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); - // Fetch the (frameCount/2)th frame. - assertThat(decoder.nthFrame(image.frameCount / 2, bitmap)).isEqualTo(AVIF_RESULT_OK); - // Fetch the last frame. - assertThat(decoder.nthFrame(image.frameCount - 1, bitmap)).isEqualTo(AVIF_RESULT_OK); - // Now nextFrame should return false. - assertThat(decoder.nextFrameIndex()).isEqualTo(image.frameCount); - assertThat(decoder.nextFrame(bitmap)).isNotEqualTo(AVIF_RESULT_OK); - // Passing out of bound values for n should fail. - assertThat(decoder.nthFrame(-1, bitmap)).isNotEqualTo(AVIF_RESULT_OK); - assertThat(decoder.nthFrame(image.frameCount, bitmap)).isNotEqualTo(AVIF_RESULT_OK); - - // The following block of code that tests scaling assumes that the animated image under test - // has at least 7 frames. These tests can be a bit slow on emulators, so only run them when - // config is ARGB_8888. - if (image.frameCount >= 7 && config == Config.ARGB_8888) { - // Reset the decoder to the first frame. - assertThat(decoder.nthFrame(0, bitmap)).isEqualTo(AVIF_RESULT_OK); - for (float scaleFactor : SCALE_FACTORS) { - // Scale both width and height. - bitmap = - Bitmap.createBitmap( - (int) (image.width * scaleFactor), (int) (image.height * scaleFactor), config); - assertThat(bitmap).isNotNull(); - assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); - - // Scale width only. - bitmap = Bitmap.createBitmap((int) (image.width * scaleFactor), image.height, config); - assertThat(bitmap).isNotNull(); - assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); - - // Scale height only. - bitmap = Bitmap.createBitmap(image.width, (int) (image.height * scaleFactor), config); - assertThat(bitmap).isNotNull(); - assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); - } - } - } - decoder.release(); - } - @Test public void testDecodeToHardwareBuffer() throws IOException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return; } - if (image.isAnimated || config != Config.ARGB_8888) { + if (config != Config.ARGB_8888) { return; } ByteBuffer buffer = image.getBuffer(); @@ -299,36 +183,6 @@ public void testDecodeToHardwareBuffer() throws IOException { } } - @Test - public void testDecodeToHardwareBufferRegularClass() throws IOException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - return; - } - if (config != Config.ARGB_8888) { - return; - } - ByteBuffer buffer = image.getBuffer(); - assertThat(buffer).isNotNull(); - AvifDecoder decoder = AvifDecoder.create(buffer, image.threads); - assertThat(decoder).isNotNull(); - for (int i = 0; i < image.frameCount; ++i) { - assertThat(decoder.nextFrameIndex()).isEqualTo(i); - HardwareBuffer hardwareBuffer = AvifHardwareDecoder.nextFrameHardwareBuffer( - decoder.getNativeDecoderHandle()); - assertThat(hardwareBuffer).isNotNull(); - assertThat(hardwareBuffer.getWidth()).isEqualTo(image.width); - assertThat(hardwareBuffer.getHeight()).isEqualTo(image.height); - hardwareBuffer.close(); - } - if (image.isAnimated) { - HardwareBuffer hardwareBuffer = - AvifHardwareDecoder.nthFrameHardwareBuffer(decoder.getNativeDecoderHandle(), 0); - assertThat(hardwareBuffer).isNotNull(); - hardwareBuffer.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 5682b6d751..9d70b4bea0 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,41 +4,20 @@ package org.aomedia.avif.android; import android.graphics.Bitmap; -import androidx.annotation.Nullable; import java.nio.ByteBuffer; /** - * An AVIF Decoder. AVIF Specification: https://aomediacodec.github.io/av1-avif/. + * A static utility class for decoding still AVIF images. * - *

There are two ways to use this class. - * - *

1) As a static utility class. - * - *

This class can be accessed statically without instantiating an object. This is useful to - * simply sniff and decode still AVIF images without having to maintain any decoder state. The - * following are the methods that can be accessed this way: {@link isAvifImage}, {@link getInfo} and - * {@link decode}. The {@link Info} inner class is used only in this case. + *

AVIF Specification: https://aomediacodec.github.io/av1-avif/. * *

For direct {@link android.hardware.HardwareBuffer} output on API 29+, use {@link * AvifHardwareDecoder} instead. - * - *

2) As an instantiated regular class. - * - *

When used this way, the {@link create} method must be used to create an instance of this class - * with a valid AVIF image. This will create a long running underlying decoder object which will be - * used to decode the image(s). Using the returned object, other public methods of the class can be - * called to get information about the image and to get the individual decoded frames. When the - * decoder object is no longer needed, {@link release} must be called to release the underlying - * decoder. - * - *

This is useful for decoding animated AVIF images and obtaining each decoded frame one after - * the other. - * - *

NOTE: The API for using this as an instantiated regular class is still under development and - * might change. */ @SuppressWarnings("CatchAndPrintStackTrace") -public class AvifDecoder { +public final class AvifDecoder { + private AvifDecoder() {} + static { try { System.loadLibrary("avif_android"); @@ -47,19 +26,6 @@ public class AvifDecoder { } } - private long decoder; - private int width; - private int height; - private int depth; - private boolean alphaPresent; - private int frameCount; - private int repetitionCount; - private double[] frameDurations; - - private AvifDecoder(ByteBuffer encoded, int threads) { - decoder = createDecoder(encoded, encoded.remaining(), threads); - } - /** Contains information about the AVIF Image. This class is only used for getInfo(). */ public static class Info { public int width; @@ -121,141 +87,10 @@ public static boolean decode(ByteBuffer encoded, int length, Bitmap bitmap) { */ public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads); - /** - * Returns the native decoder handle used by {@link AvifHardwareDecoder} on API 29+. - * - *

Do not call {@link #release()} until all hardware-buffer decodes using this handle have - * finished. - */ - long getNativeDecoderHandle() { - return decoder; - } - - /** Get the width of the image. */ - public int getWidth() { - return width; - } - - /** Get the height of the image. */ - public int getHeight() { - return height; - } - - /** Get the depth (bit depth) of the image. */ - public int getDepth() { - return depth; - } - - /** Returns true if the image contains a transparency/alpha channel, false otherwise. */ - public boolean getAlphaPresent() { - return alphaPresent; - } - - /** Get the number of frames in the image. */ - public int getFrameCount() { - return frameCount; - } - - /** - * Get the number of repetitions for an animated image (see repetitionCount in avif.h for - * details). - */ - public int getRepetitionCount() { - return repetitionCount; - } - - /** Get the duration for each frame in the image. */ - public double[] getFrameDurations() { - return frameDurations; - } - - /** Releases the underlying decoder object. */ - public void release() { - if (decoder != 0) { - destroyDecoder(decoder); - } - decoder = 0; - } - - /** - * Create and return an AvifDecoder. - * - * @param encoded The encoded AVIF image. encoded.position() must be 0. The memory of this - * ByteBuffer must be kept alive until release() is called. - * @return null on failure. AvifDecoder object on success. - */ - @Nullable - public static AvifDecoder create(ByteBuffer encoded) { - return create(encoded, /* threads= */ 1); - } - - /** - * Create and return an AvifDecoder with the specified number of threads. - * - * @param encoded The encoded AVIF image. encoded.position() must be 0. The memory of this - * ByteBuffer must be kept alive until release() is called. - * @param threads Number of threads to be used by the decoder. Zero means use number of CPU cores - * as the thread count. Negative values are invalid. When this value is > 0, it is simply - * mapped to the maxThreads parameter in libavif. For more details, see the documentation for - * maxThreads variable in avif.h. - * @return null on failure. AvifDecoder object on success. - */ - @Nullable - public static AvifDecoder create(ByteBuffer encoded, int threads) { - AvifDecoder decoder = new AvifDecoder(encoded, threads); - return (decoder.decoder == 0) ? null : decoder; - } - - /** - * Decodes the next frame of the animated AVIF into the bitmap. - * - * @param bitmap The decoded pixels will be copied into the bitmap. - * @return 0 (AVIF_RESULT_OK) on success and some other avifResult on failure. For a list of all - * possible status codes, see the avifResult enum on avif.h in libavif's C source code. A - * String describing the return value can be obtained by calling {@link resultToString} with - * the return value of this function. - */ - public int nextFrame(Bitmap bitmap) { - return nextFrame(decoder, bitmap); - } - - private native int nextFrame(long decoder, Bitmap bitmap); - - /** - * Get the 0-based index of the frame that will be returned by the next call to {@link nextFrame}. - * If the returned value is same as {@link getFrameCount}, then the next call to {@link nextFrame} - * will fail. - */ - public int nextFrameIndex() { - return nextFrameIndex(decoder); - } - - private native int nextFrameIndex(long decoder); - - /** - * Decodes the nth frame of the animated AVIF into the bitmap. - * - *

Note that calling this method will change the behavior of subsequent calls to {@link - * nextFrame}. {@link nextFrame} will start outputting the frame after this one. - * - * @param bitmap The decoded pixels will be copied into the bitmap. - * @param n The zero-based index of the frame to be decoded. - * @return 0 (AVIF_RESULT_OK) on success and some other avifResult on failure. For a list of all - * possible status codes, see the avifResult enum on avif.h in libavif's C source code. A - * String describing the return value can be obtained by calling {@link resultToString} with - * the return value of this function. - */ - public int nthFrame(int n, Bitmap bitmap) { - return nthFrame(decoder, n, bitmap); - } - - private native int nthFrame(long decoder, int n, Bitmap bitmap); - /** * Returns a String describing an avifResult enum value. * - * @param result The avifResult value. Typically this is the return value of {@link nextFrame} or - * {@link nthFrame}. + * @param result The avifResult value. * @return A String containing the description of the avifResult. */ public static native String resultToString(int result); @@ -265,8 +100,4 @@ public int nthFrame(int n, Bitmap bitmap) { * libyuv version (if available). */ public static native String versionString(); - - private native long createDecoder(ByteBuffer encoded, int length, int threads); - - private native void destroyDecoder(long decoder); } diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java index ede8ccaec5..93e3251088 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -61,49 +61,6 @@ public static HardwareBuffer decodeToHardwareBuffer(ByteBuffer encoded, int leng return decodeToHardwareBuffer(encoded, length, 0, 0, threads); } - /** - * Decodes the next frame of an animated AVIF into an {@link HardwareBuffer}. - * - * @param nativeDecoderHandle Native decoder handle obtained from {@link - * AvifDecoder#getNativeDecoderHandle()}. - */ - @Nullable - public static HardwareBuffer nextFrameHardwareBuffer( - long nativeDecoderHandle, int targetWidth, int targetHeight) { - return nextFrameHardwareBufferNative(nativeDecoderHandle, targetWidth, targetHeight); - } - - /** Decodes the next frame at the cropped image dimensions. */ - @Nullable - public static HardwareBuffer nextFrameHardwareBuffer(long nativeDecoderHandle) { - return nextFrameHardwareBuffer(nativeDecoderHandle, 0, 0); - } - - /** - * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. - * - * @param nativeDecoderHandle Native decoder handle obtained from {@link - * AvifDecoder#getNativeDecoderHandle()}. - * @param n The zero-based index of the frame to be decoded. - */ - @Nullable - public static HardwareBuffer nthFrameHardwareBuffer( - long nativeDecoderHandle, int n, int targetWidth, int targetHeight) { - return nthFrameHardwareBufferNative(nativeDecoderHandle, n, targetWidth, targetHeight); - } - - /** Decodes the nth frame at the cropped image dimensions. */ - @Nullable - public static HardwareBuffer nthFrameHardwareBuffer(long nativeDecoderHandle, int n) { - return nthFrameHardwareBuffer(nativeDecoderHandle, n, 0, 0); - } - private static native HardwareBuffer decodeToHardwareBufferNative( ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads); - - private static native HardwareBuffer nextFrameHardwareBufferNative( - long nativeDecoderHandle, int targetWidth, int targetHeight); - - private static native HardwareBuffer nthFrameHardwareBufferNative( - long nativeDecoderHandle, int n, int targetWidth, int targetHeight); } diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 94756f017b..9007785a0d 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "avif/avif.h" @@ -67,12 +68,9 @@ struct HardwareBufferApi { AHardwareBuffer_unlock_fn unlock = nullptr; AHardwareBuffer_release_fn release = nullptr; AHardwareBuffer_toHardwareBuffer_fn to_hardware_buffer = nullptr; - bool init_attempted = false; - bool init_succeeded = false; + bool available = false; }; -HardwareBufferApi g_hardware_buffer_api; - int GetDeviceApiLevel() { if (android_get_device_api_level != nullptr) { const int api_level = android_get_device_api_level(); @@ -88,41 +86,36 @@ int GetDeviceApiLevel() { return atoi(sdk_version); } -bool EnsureHardwareBufferApiLoaded() { - if (g_hardware_buffer_api.init_attempted) { - return g_hardware_buffer_api.init_succeeded; - } - g_hardware_buffer_api.init_attempted = true; - +void LoadHardwareBufferApi(HardwareBufferApi* api) { if (GetDeviceApiLevel() < 29) { LOGE("HardwareBuffer decode requires API 29+."); - return false; + return; } - g_hardware_buffer_api.android_library = dlopen("libandroid.so", RTLD_NOW); - if (g_hardware_buffer_api.android_library == nullptr) { + api->android_library = dlopen("libandroid.so", RTLD_NOW); + if (api->android_library == nullptr) { LOGE("Failed to dlopen libandroid.so: %s.", dlerror()); - return false; + return; } - g_hardware_buffer_api.nativewindow_library = dlopen("libnativewindow.so", RTLD_NOW); - if (g_hardware_buffer_api.nativewindow_library == nullptr) { + api->nativewindow_library = dlopen("libnativewindow.so", RTLD_NOW); + if (api->nativewindow_library == nullptr) { LOGE("Failed to dlopen libnativewindow.so: %s.", dlerror()); - dlclose(g_hardware_buffer_api.android_library); - g_hardware_buffer_api.android_library = nullptr; - return false; + dlclose(api->android_library); + api->android_library = nullptr; + return; } #define LOAD_ANDROID_SYMBOL(name) \ - g_hardware_buffer_api.name = reinterpret_cast( \ - dlsym(g_hardware_buffer_api.android_library, "AHardwareBuffer_" #name)); \ - if (g_hardware_buffer_api.name == nullptr) { \ - LOGE("Failed to dlsym AHardwareBuffer_" #name ": %s.", dlerror()); \ - dlclose(g_hardware_buffer_api.nativewindow_library); \ - dlclose(g_hardware_buffer_api.android_library); \ - g_hardware_buffer_api.nativewindow_library = nullptr; \ - g_hardware_buffer_api.android_library = nullptr; \ - return false; \ + api->name = reinterpret_cast( \ + dlsym(api->android_library, "AHardwareBuffer_" #name)); \ + if (api->name == nullptr) { \ + LOGE("Failed to dlsym AHardwareBuffer_" #name ": %s.", dlerror()); \ + dlclose(api->nativewindow_library); \ + dlclose(api->android_library); \ + api->nativewindow_library = nullptr; \ + api->android_library = nullptr; \ + return; \ } LOAD_ANDROID_SYMBOL(allocate); @@ -133,20 +126,25 @@ bool EnsureHardwareBufferApiLoaded() { #undef LOAD_ANDROID_SYMBOL - g_hardware_buffer_api.to_hardware_buffer = - reinterpret_cast(dlsym( - g_hardware_buffer_api.nativewindow_library, "AHardwareBuffer_toHardwareBuffer")); - if (g_hardware_buffer_api.to_hardware_buffer == nullptr) { + api->to_hardware_buffer = reinterpret_cast( + dlsym(api->nativewindow_library, "AHardwareBuffer_toHardwareBuffer")); + if (api->to_hardware_buffer == nullptr) { LOGE("Failed to dlsym AHardwareBuffer_toHardwareBuffer: %s.", dlerror()); - dlclose(g_hardware_buffer_api.nativewindow_library); - dlclose(g_hardware_buffer_api.android_library); - g_hardware_buffer_api.nativewindow_library = nullptr; - g_hardware_buffer_api.android_library = nullptr; - return false; + dlclose(api->nativewindow_library); + dlclose(api->android_library); + api->nativewindow_library = nullptr; + api->android_library = nullptr; + return; } - g_hardware_buffer_api.init_succeeded = true; - return true; + api->available = true; +} + +const HardwareBufferApi& GetHardwareBufferApi() { + static std::once_flag init_flag; + static HardwareBufferApi api; + std::call_once(init_flag, []() { LoadHardwareBufferApi(&api); }); + return api; } // RAII wrapper class that properly frees the decoder related objects on @@ -416,7 +414,8 @@ avifResult AvifImageToBitmap(JNIEnv* const env, jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t dst_width, uint32_t dst_height) { - if (!EnsureHardwareBufferApiLoaded()) { + const HardwareBufferApi& hw_api = GetHardwareBufferApi(); + if (!hw_api.available) { return nullptr; } @@ -429,17 +428,17 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; AHardwareBuffer* hw_buffer = nullptr; - if (g_hardware_buffer_api.allocate(&desc, &hw_buffer) != 0) { + if (hw_api.allocate(&desc, &hw_buffer) != 0) { LOGE("AHardwareBuffer_allocate failed."); return nullptr; } - g_hardware_buffer_api.describe(hw_buffer, &desc); + hw_api.describe(hw_buffer, &desc); void* pixels = nullptr; - if (g_hardware_buffer_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, - -1, nullptr, &pixels) != 0) { + if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, + &pixels) != 0) { LOGE("AHardwareBuffer_lock failed."); - g_hardware_buffer_api.release(hw_buffer); + hw_api.release(hw_buffer); return nullptr; } @@ -447,14 +446,14 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, decoder, pixels, dst_width, dst_height, static_cast(desc.stride) * 4, AVIF_RGB_FORMAT_RGBA, 8, AVIF_FALSE); - g_hardware_buffer_api.unlock(hw_buffer, nullptr); + hw_api.unlock(hw_buffer, nullptr); if (res != AVIF_RESULT_OK) { - g_hardware_buffer_api.release(hw_buffer); + hw_api.release(hw_buffer); return nullptr; } - jobject java_buffer = g_hardware_buffer_api.to_hardware_buffer(env, hw_buffer); - g_hardware_buffer_api.release(hw_buffer); + jobject java_buffer = hw_api.to_hardware_buffer(env, hw_buffer); + hw_api.release(hw_buffer); if (java_buffer == nullptr) { LOGE("AHardwareBuffer_toHardwareBuffer failed."); if (JniExceptionCheck(env)) { @@ -488,36 +487,6 @@ jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height); } -jobject NextFrameToHardwareBuffer(JNIEnv* const env, - AvifDecoderWrapper* const decoder, - int target_width, int target_height) { - const avifResult decode_result = avifDecoderNextImage(decoder->decoder); - if (decode_result != AVIF_RESULT_OK) { - LOGE("Failed to decode AVIF image. Status: %d", decode_result); - return nullptr; - } - uint32_t dst_width = 0; - uint32_t dst_height = 0; - GetTargetDimensions(decoder, target_width, target_height, &dst_width, - &dst_height); - return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); -} - -jobject NthFrameToHardwareBuffer(JNIEnv* const env, - AvifDecoderWrapper* const decoder, uint32_t n, - int target_width, int target_height) { - const avifResult decode_result = avifDecoderNthImage(decoder->decoder, n); - if (decode_result != AVIF_RESULT_OK) { - LOGE("Failed to decode AVIF image. Status: %d", decode_result); - return nullptr; - } - uint32_t dst_width = 0; - uint32_t dst_height = 0; - GetTargetDimensions(decoder, target_width, target_height, &dst_width, - &dst_height); - return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); -} - avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, jobject bitmap) { avifResult res = avifDecoderNextImage(decoder->decoder); @@ -528,16 +497,6 @@ avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, return AvifImageToBitmap(env, decoder, bitmap); } -avifResult DecodeNthImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, - uint32_t n, jobject bitmap) { - avifResult res = avifDecoderNthImage(decoder->decoder, n); - if (res != AVIF_RESULT_OK) { - LOGE("Failed to decode AVIF image. Status: %d", res); - return res; - } - return AvifImageToBitmap(env, decoder, bitmap); -} - } // namespace jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { @@ -617,97 +576,6 @@ FUNC(jboolean, decode, jobject encoded, int length, jobject bitmap, return DecodeNextImage(env, &decoder, bitmap) == AVIF_RESULT_OK; } -FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) { - const uint8_t* buffer = nullptr; - size_t size = 0; - if (!ValidateDirectBuffer(env, encoded, length, &buffer, &size)) { - return 0; - } - std::unique_ptr decoder(new (std::nothrow) - AvifDecoderWrapper()); - if (decoder == nullptr) { - return 0; - } - if (!CreateDecoderAndParse(decoder.get(), buffer, size, - getThreadCount(threads))) { - return 0; - } - FIND_CLASS(avif_decoder_class, "org/aomedia/avif/android/AvifDecoder", 0); - GET_FIELD_ID(width_id, avif_decoder_class, "width", "I", 0); - GET_FIELD_ID(height_id, avif_decoder_class, "height", "I", 0); - GET_FIELD_ID(depth_id, avif_decoder_class, "depth", "I", 0); - GET_FIELD_ID(alpha_present_id, avif_decoder_class, "alphaPresent", "Z", 0); - GET_FIELD_ID(frame_count_id, avif_decoder_class, "frameCount", "I", 0); - GET_FIELD_ID(repetition_count_id, avif_decoder_class, "repetitionCount", "I", - 0); - GET_FIELD_ID(frame_durations_id, avif_decoder_class, "frameDurations", "[D", - 0); - env->SetIntField(thiz, width_id, decoder->crop.width); - CHECK_EXCEPTION(0); - env->SetIntField(thiz, height_id, decoder->crop.height); - CHECK_EXCEPTION(0); - env->SetIntField(thiz, depth_id, decoder->decoder->image->depth); - CHECK_EXCEPTION(0); - env->SetBooleanField(thiz, alpha_present_id, decoder->decoder->alphaPresent); - CHECK_EXCEPTION(0); - env->SetIntField(thiz, repetition_count_id, - decoder->decoder->repetitionCount); - CHECK_EXCEPTION(0); - const int frameCount = decoder->decoder->imageCount; - env->SetIntField(thiz, frame_count_id, frameCount); - CHECK_EXCEPTION(0); - // This native array is needed because setting one element at a time to a Java - // array from the JNI layer is inefficient. - std::unique_ptr native_durations( - new (std::nothrow) double[frameCount]); - if (native_durations == nullptr) { - return 0; - } - for (int i = 0; i < frameCount; ++i) { - avifImageTiming timing; - if (avifDecoderNthImageTiming(decoder->decoder, i, &timing) != - AVIF_RESULT_OK) { - return 0; - } - native_durations[i] = timing.duration; - } - jdoubleArray durations = env->NewDoubleArray(frameCount); - if (durations == nullptr) { - return 0; - } - env->SetDoubleArrayRegion(durations, /*start=*/0, frameCount, - native_durations.get()); - CHECK_EXCEPTION(0); - env->SetObjectField(thiz, frame_durations_id, durations); - CHECK_EXCEPTION(0); - return reinterpret_cast(decoder.release()); -} - -#undef GET_FIELD_ID -#undef FIND_CLASS -#undef CHECK_EXCEPTION - -FUNC(jint, nextFrame, jlong jdecoder, jobject bitmap) { - IGNORE_UNUSED_JNI_PARAMETERS; - AvifDecoderWrapper* const decoder = - reinterpret_cast(jdecoder); - return DecodeNextImage(env, decoder, bitmap); -} - -FUNC(jint, nextFrameIndex, jlong jdecoder) { - IGNORE_UNUSED_JNI_PARAMETERS; - AvifDecoderWrapper* const decoder = - reinterpret_cast(jdecoder); - return decoder->decoder->imageIndex + 1; -} - -FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { - IGNORE_UNUSED_JNI_PARAMETERS; - AvifDecoderWrapper* const decoder = - reinterpret_cast(jdecoder); - return DecodeNthImage(env, decoder, n, bitmap); -} - HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, jint target_width, jint target_height, jint threads) { IGNORE_UNUSED_HW_JNI_PARAMETERS; @@ -715,23 +583,6 @@ HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, target_height, threads); } -HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_width, - jint target_height) { - IGNORE_UNUSED_HW_JNI_PARAMETERS; - AvifDecoderWrapper* const decoder = - reinterpret_cast(jdecoder); - return NextFrameToHardwareBuffer(env, decoder, target_width, target_height); -} - -HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, - jint target_width, jint target_height) { - IGNORE_UNUSED_HW_JNI_PARAMETERS; - AvifDecoderWrapper* const decoder = - reinterpret_cast(jdecoder); - return NthFrameToHardwareBuffer(env, decoder, static_cast(n), - target_width, target_height); -} - FUNC(jstring, resultToString, jint result) { IGNORE_UNUSED_JNI_PARAMETERS; return env->NewStringUTF(avifResultToString(static_cast(result))); @@ -753,10 +604,3 @@ FUNC(jstring, versionString) { avifVersion(), codec_versions, libyuv_version); return env->NewStringUTF(version_string); } - -FUNC(void, destroyDecoder, jlong jdecoder) { - IGNORE_UNUSED_JNI_PARAMETERS; - AvifDecoderWrapper* const decoder = - reinterpret_cast(jdecoder); - delete decoder; -} From 532ff8a7b16e9093e2e6f58be1984ff700861d4b Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 11 Jul 2026 23:41:36 +0900 Subject: [PATCH 04/25] =?UTF-8?q?dl=E3=81=AE=E5=8F=96=E5=BE=97=E5=85=83?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/jni/libavif_jni.cc | 64 +++++++------------ 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 9007785a0d..fad1d67f46 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -60,8 +60,7 @@ using AHardwareBuffer_release_fn = void (*)(AHardwareBuffer*); using AHardwareBuffer_toHardwareBuffer_fn = jobject (*)(JNIEnv*, AHardwareBuffer*); struct HardwareBufferApi { - void* android_library = nullptr; - void* nativewindow_library = nullptr; + void* library = nullptr; AHardwareBuffer_allocate_fn allocate = nullptr; AHardwareBuffer_describe_fn describe = nullptr; AHardwareBuffer_lock_fn lock = nullptr; @@ -92,51 +91,34 @@ void LoadHardwareBufferApi(HardwareBufferApi* api) { return; } - api->android_library = dlopen("libandroid.so", RTLD_NOW); - if (api->android_library == nullptr) { - LOGE("Failed to dlopen libandroid.so: %s.", dlerror()); - return; - } - - api->nativewindow_library = dlopen("libnativewindow.so", RTLD_NOW); - if (api->nativewindow_library == nullptr) { + // All AHardwareBuffer_* symbols used here (including the JNI conversion + // helper) live in libnativewindow.so. Load them from a single library so + // symbol resolution stays consistent across API levels. + api->library = dlopen("libnativewindow.so", RTLD_NOW); + if (api->library == nullptr) { LOGE("Failed to dlopen libnativewindow.so: %s.", dlerror()); - dlclose(api->android_library); - api->android_library = nullptr; return; } -#define LOAD_ANDROID_SYMBOL(name) \ - api->name = reinterpret_cast( \ - dlsym(api->android_library, "AHardwareBuffer_" #name)); \ - if (api->name == nullptr) { \ - LOGE("Failed to dlsym AHardwareBuffer_" #name ": %s.", dlerror()); \ - dlclose(api->nativewindow_library); \ - dlclose(api->android_library); \ - api->nativewindow_library = nullptr; \ - api->android_library = nullptr; \ - return; \ - } - - LOAD_ANDROID_SYMBOL(allocate); - LOAD_ANDROID_SYMBOL(describe); - LOAD_ANDROID_SYMBOL(lock); - LOAD_ANDROID_SYMBOL(unlock); - LOAD_ANDROID_SYMBOL(release); - -#undef LOAD_ANDROID_SYMBOL - - api->to_hardware_buffer = reinterpret_cast( - dlsym(api->nativewindow_library, "AHardwareBuffer_toHardwareBuffer")); - if (api->to_hardware_buffer == nullptr) { - LOGE("Failed to dlsym AHardwareBuffer_toHardwareBuffer: %s.", dlerror()); - dlclose(api->nativewindow_library); - dlclose(api->android_library); - api->nativewindow_library = nullptr; - api->android_library = nullptr; - return; +#define LOAD_SYMBOL(field, symbol) \ + api->field = reinterpret_castfield)>(dlsym(api->library, \ + symbol)); \ + if (api->field == nullptr) { \ + LOGE("Failed to dlsym %s: %s.", symbol, dlerror()); \ + dlclose(api->library); \ + api->library = nullptr; \ + return; \ } + LOAD_SYMBOL(allocate, "AHardwareBuffer_allocate"); + LOAD_SYMBOL(describe, "AHardwareBuffer_describe"); + LOAD_SYMBOL(lock, "AHardwareBuffer_lock"); + LOAD_SYMBOL(unlock, "AHardwareBuffer_unlock"); + LOAD_SYMBOL(release, "AHardwareBuffer_release"); + LOAD_SYMBOL(to_hardware_buffer, "AHardwareBuffer_toHardwareBuffer"); + +#undef LOAD_SYMBOL + api->available = true; } From ef78ba84897a00d6bc0b78da193fd599d70af47d Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sun, 12 Jul 2026 00:04:53 +0900 Subject: [PATCH 05/25] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E3=81=AE=E6=8C=87=E6=91=98=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/README.md | 8 + android_jni/avifandroidjni/proguard-rules.pro | 2 +- .../AvifDecoderLengthValidationTest.java | 37 +++- .../aomedia/avif/android/AvifDecoderTest.java | 184 +++++++++++++++++- .../org/aomedia/avif/android/AvifDecoder.java | 180 ++++++++++++++++- .../avif/android/AvifHardwareDecoder.java | 69 ++++++- .../src/main/jni/libavif_jni.cc | 159 +++++++++++++++ 7 files changed, 621 insertions(+), 18 deletions(-) diff --git a/android_jni/README.md b/android_jni/README.md index bc32e9b409..d57371c4b5 100644 --- a/android_jni/README.md +++ b/android_jni/README.md @@ -42,6 +42,10 @@ $ cd .. The Android dav1d build is configured with `-Dbitdepths=8` (8-bit AV1 only). Re-run the script after changing this option so that all ABIs are rebuilt. +Instrumented tests assume this 8-bit-only dav1d build: 10/12-bit assets are still parsed via +`getInfo`, but decode APIs are expected to fail for those streams. If you rebuild dav1d with full +bitdepths (`-Dbitdepths=8,16`), update the tests accordingly. + If you want to use libgav1 instead: ``` @@ -86,6 +90,10 @@ Step 1 - Build the library Make sure to build the library by following the steps under [Generate the AAR package](#generate-the-aar-package) section above. +These tests assume the default Android dav1d build (`-Dbitdepths=8`). Decode success +cases cover 8-bit images only; 10/12-bit assets verify `getInfo` and clean decode +failure. + Step 2 - Set up a device/emulator Make sure that a device or an emulator has been set up and is available via diff --git a/android_jni/avifandroidjni/proguard-rules.pro b/android_jni/avifandroidjni/proguard-rules.pro index c6b330f6c6..a3052713eb 100644 --- a/android_jni/avifandroidjni/proguard-rules.pro +++ b/android_jni/avifandroidjni/proguard-rules.pro @@ -10,7 +10,7 @@ # Members of these classes may be accessed from native methods. Keep them # unobfuscated. -keep class org.aomedia.avif.android.AvifDecoder { - public *; + *; } -keep class org.aomedia.avif.android.AvifDecoder$Info { *; diff --git a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java index 3f5f12e655..706a25bf7e 100644 --- a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java +++ b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java @@ -7,14 +7,17 @@ // getInfo(...) -> false // decode(...) -> false // isAvifImage(...) -> false +// create(...) -> null // // Happy-path cases in this file exist to guard against the new validation // accidentally over-rejecting legitimate inputs (length == capacity, -// length == 0). +// length == 0, valid images through create()). package org.aomedia.avif.android; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.content.res.AssetManager; @@ -262,6 +265,38 @@ public void isAvifImage_emptyDirectBuffer_returnsFalseNoCrash() { assertFalse(AvifDecoder.isAvifImage(buf)); } + // --------------------------------------------------------------------------- + // create (AvifDecoder factory). Uses encoded.remaining() internally, so the + // length branch can't be poisoned from the public Java surface. These tests + // guard against the createDecoder hardening accidentally breaking the happy + // path or the clean-failure contract on malformed input. + // --------------------------------------------------------------------------- + + @Test + public void create_truncatedFtypDirect_returnsNull() { + ByteBuffer buf = tinyFtypDirectBuffer(); + assertNull(AvifDecoder.create(buf)); + } + + @Test + public void create_heapBackedBuffer_returnsNull() { + ByteBuffer buf = tinyFtypHeapBuffer(); + assertNull(AvifDecoder.create(buf)); + } + + @Test + public void create_emptyDirectBuffer_returnsNull() { + ByteBuffer buf = emptyDirectBuffer(); + assertNull(AvifDecoder.create(buf)); + } + + @Test + public void create_validImage_stillReturnsNonNull() throws IOException { + ByteBuffer buf = loadDirectAssetBuffer("avif/fox.profile0.8bpc.yuv420.avif"); + AvifDecoder decoder = AvifDecoder.create(buf); + assertNotNull(decoder); + } + @Test public void getInfo_validImageHonestLength_returnsTrue() throws IOException { // Sanity: after hardening, the honest-length path on a well-formed image 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 9bfd952d75..f00033ab8f 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 @@ -23,7 +23,13 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; -/** Instrumentation tests for the libavif JNI API, which will execute on an Android device. */ +/** + * Instrumentation tests for the libavif JNI API, which will execute on an Android device. + * + *

The Android dav1d build uses {@code -Dbitdepths=8}, so pixel decoding is exercised only for + * 8-bit images. 10/12-bit assets are still used to verify header parsing ({@link + * AvifDecoder#getInfo}) and that decode paths fail cleanly. + */ @RunWith(Parameterized.class) public class AvifDecoderTest { @@ -34,7 +40,11 @@ private static class Image { public final int height; public final int depth; public final boolean alphaPresent; + public final int frameCount; + public final int repetitionCount; + public final double frameDuration; public final int threads; + public final boolean isAnimated; public Image( String directory, @@ -44,13 +54,41 @@ public Image( int depth, boolean alphaPresent, int threads) { + this( + directory, + filename, + width, + height, + depth, + alphaPresent, + /* frameCount= */ 1, + /* repetitionCount= */ 0, + /* frameDuration= */ 0.0, + threads); + } + + public Image( + String directory, + String filename, + int width, + int height, + int depth, + boolean alphaPresent, + int frameCount, + int repetitionCount, + double frameDuration, + int threads) { this.directory = directory; this.filename = filename; this.width = width; this.height = height; this.depth = depth; this.alphaPresent = alphaPresent; + this.frameCount = frameCount; + this.repetitionCount = repetitionCount; + this.frameDuration = frameDuration; this.threads = threads; + this.isAnimated = frameCount > 1; } public ByteBuffer getBuffer() throws IOException { @@ -68,8 +106,14 @@ public ByteBuffer getBuffer() throws IOException { private static final float[] SCALE_FACTORS = {0.5f, 1.3f}; + // Matches ext/dav1d_android.sh / LocalDav1d.cmake (-Dbitdepths=8). + private boolean isDecodeSupported() { + return image.depth == 8; + } + private static final Image[] IMAGES = { - // Parameter ordering: directory, filename, width, height, depth, alphaPresent, threads. + // Parameter ordering for still images: directory, filename, width, height, depth, alphaPresent, + // threads. new Image("avif", "fox.profile0.10bpc.yuv420.avif", 1204, 800, 10, false, 1), new Image("avif", "fox.profile0.10bpc.yuv420.monochrome.avif", 1204, 800, 10, false, 1), new Image("avif", "fox.profile0.8bpc.yuv420.avif", 1204, 800, 8, false, 1), @@ -83,6 +127,11 @@ public ByteBuffer getBuffer() throws IOException { new Image("avif", "fox.profile2.12bpc.yuv444.avif", 1204, 800, 12, false, 1), new Image("avif", "fox.profile2.8bpc.yuv422.avif", 1204, 800, 8, false, 1), new Image("avif", "blue-and-magenta-crop.avif", 180, 100, 8, true, 1), + // Parameter ordering for animated images: directory, filename, width, height, depth, + // alphaPresent, frameCount, repetitionCount, frameDuration, threads. + new Image("animated_avif", "alpha_video.avif", 640, 480, 8, true, 48, -2, 0.04, 1), + new Image( + "animated_avif", "Chimera-AV1-10bit-480x270.avif", 480, 270, 10, false, 95, -2, 0.04, 2), }; @Parameters @@ -91,8 +140,9 @@ public static List data() throws IOException { for (Image image : IMAGES) { // Test ARGB_8888 for all files. list.add(new Object[] {Config.ARGB_8888, image}); - // For 8bpc files, test RGB_565. For other files, test RGBA_F16. - Config testConfig = image.depth == 8 ? Config.RGB_565 : Config.RGBA_F16; + // For 8bpc files and animated files, test only RGB_565 (F16 is flaky for animated files on + // x86 emulators). For other files, test only RGBA_F16. + Config testConfig = (image.depth == 8 || image.isAnimated) ? Config.RGB_565 : Config.RGBA_F16; list.add(new Object[] {testConfig, image}); } return list; @@ -104,8 +154,13 @@ public static List data() throws IOException { @Parameter(1) public Image image; + // Tests AvifDecoder by using it as a utility class without instantiating it. Only still images + // can be decoded this way. @Test public void testDecodeUtilityClass() throws IOException { + if (image.isAnimated) { + return; + } ByteBuffer buffer = image.getBuffer(); assertThat(buffer).isNotNull(); assertThat(AvifDecoder.isAvifImage(buffer)).isTrue(); @@ -117,6 +172,11 @@ public void testDecodeUtilityClass() throws IOException { assertThat(info.alphaPresent).isEqualTo(image.alphaPresent); Bitmap bitmap = Bitmap.createBitmap(info.width, info.height, config); assertThat(bitmap).isNotNull(); + if (!isDecodeSupported()) { + // 8-bit-only dav1d cannot decode 10/12-bit streams. + assertThat(AvifDecoder.decode(buffer, buffer.remaining(), bitmap)).isFalse(); + return; + } assertThat(AvifDecoder.decode(buffer, buffer.remaining(), bitmap)).isTrue(); // Test scaling. These tests can be a bit slow on emulators, so only run them when config is @@ -143,12 +203,89 @@ public void testDecodeUtilityClass() throws IOException { } } + // Tests AvifDecoder by using it as a regular instantiated class. + @Test + public void testDecodeRegularClass() throws IOException { + ByteBuffer buffer = image.getBuffer(); + assertThat(buffer).isNotNull(); + AvifDecoder decoder = AvifDecoder.create(buffer, image.threads); + assertThat(decoder).isNotNull(); + assertThat(decoder.getWidth()).isEqualTo(image.width); + assertThat(decoder.getHeight()).isEqualTo(image.height); + assertThat(decoder.getDepth()).isEqualTo(image.depth); + assertThat(decoder.getAlphaPresent()).isEqualTo(image.alphaPresent); + assertThat(decoder.getFrameCount()).isEqualTo(image.frameCount); + Bitmap bitmap = Bitmap.createBitmap(image.width, image.height, config); + assertThat(bitmap).isNotNull(); + if (!isDecodeSupported()) { + assertThat(decoder.nextFrame(bitmap)).isNotEqualTo(AVIF_RESULT_OK); + decoder.release(); + return; + } + for (int i = 0; i < image.frameCount; i++) { + assertThat(decoder.nextFrameIndex()).isEqualTo(i); + assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); + } + if (image.isAnimated) { + assertThat(decoder.getRepetitionCount()).isEqualTo(image.repetitionCount); + double[] frameDurations = decoder.getFrameDurations(); + assertThat(frameDurations).isNotNull(); + assertThat(frameDurations).hasLength(image.frameCount); + for (int i = 0; i < image.frameCount; i++) { + assertThat(frameDurations[i]).isWithin(1.0e-2).of(image.frameDuration); + } + assertThat(decoder.nextFrameIndex()).isEqualTo(image.frameCount); + // Fetch the first frame again. + assertThat(decoder.nthFrame(0, bitmap)).isEqualTo(AVIF_RESULT_OK); + // Now nextFrame will return the second frame. + assertThat(decoder.nextFrameIndex()).isEqualTo(1); + assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); + // Fetch the (frameCount/2)th frame. + assertThat(decoder.nthFrame(image.frameCount / 2, bitmap)).isEqualTo(AVIF_RESULT_OK); + // Fetch the last frame. + assertThat(decoder.nthFrame(image.frameCount - 1, bitmap)).isEqualTo(AVIF_RESULT_OK); + // Now nextFrame should return false. + assertThat(decoder.nextFrameIndex()).isEqualTo(image.frameCount); + assertThat(decoder.nextFrame(bitmap)).isNotEqualTo(AVIF_RESULT_OK); + // Passing out of bound values for n should fail. + assertThat(decoder.nthFrame(-1, bitmap)).isNotEqualTo(AVIF_RESULT_OK); + assertThat(decoder.nthFrame(image.frameCount, bitmap)).isNotEqualTo(AVIF_RESULT_OK); + + // The following block of code that tests scaling assumes that the animated image under test + // has at least 7 frames. These tests can be a bit slow on emulators, so only run them when + // config is ARGB_8888. + if (image.frameCount >= 7 && config == Config.ARGB_8888) { + // Reset the decoder to the first frame. + assertThat(decoder.nthFrame(0, bitmap)).isEqualTo(AVIF_RESULT_OK); + for (float scaleFactor : SCALE_FACTORS) { + // Scale both width and height. + bitmap = + Bitmap.createBitmap( + (int) (image.width * scaleFactor), (int) (image.height * scaleFactor), config); + assertThat(bitmap).isNotNull(); + assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); + + // Scale width only. + bitmap = Bitmap.createBitmap((int) (image.width * scaleFactor), image.height, config); + assertThat(bitmap).isNotNull(); + assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); + + // Scale height only. + bitmap = Bitmap.createBitmap(image.width, (int) (image.height * scaleFactor), config); + assertThat(bitmap).isNotNull(); + assertThat(decoder.nextFrame(bitmap)).isEqualTo(AVIF_RESULT_OK); + } + } + } + decoder.release(); + } + @Test public void testDecodeToHardwareBuffer() throws IOException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { return; } - if (config != Config.ARGB_8888) { + if (image.isAnimated || config != Config.ARGB_8888) { return; } ByteBuffer buffer = image.getBuffer(); @@ -158,6 +295,10 @@ public void testDecodeToHardwareBuffer() throws IOException { HardwareBuffer hardwareBuffer = AvifHardwareDecoder.decodeToHardwareBuffer(buffer, buffer.remaining(), image.threads); + if (!isDecodeSupported()) { + assertThat(hardwareBuffer).isNull(); + return; + } assertThat(hardwareBuffer).isNotNull(); assertThat(hardwareBuffer.getWidth()).isEqualTo(info.width); assertThat(hardwareBuffer.getHeight()).isEqualTo(info.height); @@ -183,6 +324,39 @@ public void testDecodeToHardwareBuffer() throws IOException { } } + @Test + public void testDecodeToHardwareBufferRegularClass() throws IOException { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return; + } + if (config != Config.ARGB_8888) { + return; + } + ByteBuffer buffer = image.getBuffer(); + assertThat(buffer).isNotNull(); + AvifDecoder decoder = AvifDecoder.create(buffer, image.threads); + assertThat(decoder).isNotNull(); + if (!isDecodeSupported()) { + assertThat(AvifHardwareDecoder.nextFrameHardwareBuffer(decoder)).isNull(); + decoder.release(); + return; + } + for (int i = 0; i < image.frameCount; ++i) { + assertThat(decoder.nextFrameIndex()).isEqualTo(i); + HardwareBuffer hardwareBuffer = AvifHardwareDecoder.nextFrameHardwareBuffer(decoder); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getWidth()).isEqualTo(image.width); + assertThat(hardwareBuffer.getHeight()).isEqualTo(image.height); + hardwareBuffer.close(); + } + if (image.isAnimated) { + HardwareBuffer hardwareBuffer = AvifHardwareDecoder.nthFrameHardwareBuffer(decoder, 0); + assertThat(hardwareBuffer).isNotNull(); + hardwareBuffer.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 9d70b4bea0..195d242148 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,20 +4,41 @@ package org.aomedia.avif.android; import android.graphics.Bitmap; +import androidx.annotation.Nullable; import java.nio.ByteBuffer; /** - * A static utility class for decoding still AVIF images. + * An AVIF Decoder. AVIF Specification: https://aomediacodec.github.io/av1-avif/. * - *

AVIF Specification: https://aomediacodec.github.io/av1-avif/. + *

There are two ways to use this class. + * + *

1) As a static utility class. + * + *

This class can be accessed statically without instantiating an object. This is useful to + * simply sniff and decode still AVIF images without having to maintain any decoder state. The + * following are the methods that can be accessed this way: {@link isAvifImage}, {@link getInfo} and + * {@link decode}. The {@link Info} inner class is used only in this case. * *

For direct {@link android.hardware.HardwareBuffer} output on API 29+, use {@link * AvifHardwareDecoder} instead. + * + *

2) As an instantiated regular class. + * + *

When used this way, the {@link create} method must be used to create an instance of this class + * with a valid AVIF image. This will create a long running underlying decoder object which will be + * used to decode the image(s). Using the returned object, other public methods of the class can be + * called to get information about the image and to get the individual decoded frames. When the + * decoder object is no longer needed, {@link release} must be called to release the underlying + * decoder. + * + *

This is useful for decoding animated AVIF images and obtaining each decoded frame one after + * the other. + * + *

NOTE: The API for using this as an instantiated regular class is still under development and + * might change. */ @SuppressWarnings("CatchAndPrintStackTrace") -public final class AvifDecoder { - private AvifDecoder() {} - +public class AvifDecoder { static { try { System.loadLibrary("avif_android"); @@ -26,6 +47,19 @@ private AvifDecoder() {} } } + private long decoder; + private int width; + private int height; + private int depth; + private boolean alphaPresent; + private int frameCount; + private int repetitionCount; + private double[] frameDurations; + + private AvifDecoder(ByteBuffer encoded, int threads) { + decoder = createDecoder(encoded, encoded.remaining(), threads); + } + /** Contains information about the AVIF Image. This class is only used for getInfo(). */ public static class Info { public int width; @@ -87,10 +121,136 @@ public static boolean decode(ByteBuffer encoded, int length, Bitmap bitmap) { */ public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads); + /** Get the width of the image. */ + public int getWidth() { + return width; + } + + /** Get the height of the image. */ + public int getHeight() { + return height; + } + + /** Get the depth (bit depth) of the image. */ + public int getDepth() { + return depth; + } + + /** Returns true if the image contains a transparency/alpha channel, false otherwise. */ + public boolean getAlphaPresent() { + return alphaPresent; + } + + /** Get the number of frames in the image. */ + public int getFrameCount() { + return frameCount; + } + + /** + * Get the number of repetitions for an animated image (see repetitionCount in avif.h for + * details). + */ + public int getRepetitionCount() { + return repetitionCount; + } + + /** Get the duration for each frame in the image. */ + public double[] getFrameDurations() { + return frameDurations; + } + + /** Returns true if the underlying native decoder is still alive. */ + public boolean isAlive() { + return decoder != 0; + } + + /** Releases the underlying decoder object. */ + public void release() { + if (decoder != 0) { + destroyDecoder(decoder); + } + decoder = 0; + } + + /** + * Create and return an AvifDecoder. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. The memory of this + * ByteBuffer must be kept alive until release() is called. + * @return null on failure. AvifDecoder object on success. + */ + @Nullable + public static AvifDecoder create(ByteBuffer encoded) { + return create(encoded, /* threads= */ 1); + } + + /** + * Create and return an AvifDecoder with the specified number of threads. + * + * @param encoded The encoded AVIF image. encoded.position() must be 0. The memory of this + * ByteBuffer must be kept alive until release() is called. + * @param threads Number of threads to be used by the decoder. Zero means use number of CPU cores + * as the thread count. Negative values are invalid. When this value is > 0, it is simply + * mapped to the maxThreads parameter in libavif. For more details, see the documentation for + * maxThreads variable in avif.h. + * @return null on failure. AvifDecoder object on success. + */ + @Nullable + public static AvifDecoder create(ByteBuffer encoded, int threads) { + AvifDecoder decoder = new AvifDecoder(encoded, threads); + return (decoder.decoder == 0) ? null : decoder; + } + + /** + * Decodes the next frame of the animated AVIF into the bitmap. + * + * @param bitmap The decoded pixels will be copied into the bitmap. + * @return 0 (AVIF_RESULT_OK) on success and some other avifResult on failure. For a list of all + * possible status codes, see the avifResult enum on avif.h in libavif's C source code. A + * String describing the return value can be obtained by calling {@link resultToString} with + * the return value of this function. + */ + public int nextFrame(Bitmap bitmap) { + return nextFrame(decoder, bitmap); + } + + private native int nextFrame(long decoder, Bitmap bitmap); + + /** + * Get the 0-based index of the frame that will be returned by the next call to {@link nextFrame}. + * If the returned value is same as {@link getFrameCount}, then the next call to {@link nextFrame} + * will fail. + */ + public int nextFrameIndex() { + return nextFrameIndex(decoder); + } + + private native int nextFrameIndex(long decoder); + + /** + * Decodes the nth frame of the animated AVIF into the bitmap. + * + *

Note that calling this method will change the behavior of subsequent calls to {@link + * nextFrame}. {@link nextFrame} will start outputting the frame after this one. + * + * @param bitmap The decoded pixels will be copied into the bitmap. + * @param n The zero-based index of the frame to be decoded. + * @return 0 (AVIF_RESULT_OK) on success and some other avifResult on failure. For a list of all + * possible status codes, see the avifResult enum on avif.h in libavif's C source code. A + * String describing the return value can be obtained by calling {@link resultToString} with + * the return value of this function. + */ + public int nthFrame(int n, Bitmap bitmap) { + return nthFrame(decoder, n, bitmap); + } + + private native int nthFrame(long decoder, int n, Bitmap bitmap); + /** * Returns a String describing an avifResult enum value. * - * @param result The avifResult value. + * @param result The avifResult value. Typically this is the return value of {@link nextFrame} or + * {@link nthFrame}. * @return A String containing the description of the avifResult. */ public static native String resultToString(int result); @@ -100,4 +260,12 @@ public static boolean decode(ByteBuffer encoded, int length, Bitmap bitmap) { * libyuv version (if available). */ public static native String versionString(); + + long getNativeDecoderHandle() { + return decoder; + } + + private native long createDecoder(ByteBuffer encoded, int length, int threads); + + private native void destroyDecoder(long decoder); } diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java index 93e3251088..5af27a51a0 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -34,14 +34,16 @@ private AvifHardwareDecoder() {} * android.graphics.Bitmap#wrapHardwareBuffer(HardwareBuffer, * android.graphics.ColorSpace)}. * - *

If {@code targetWidth} and {@code targetHeight} are both positive, the decoded image is - * scaled to those dimensions before RGB conversion. Otherwise the cropped image dimensions are - * used. + *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. + * If either is zero or negative, the cropped image dimensions are used (partial scaling is not + * supported). * * @param encoded The encoded AVIF image. encoded.position() must be 0. * @param length Length of the encoded buffer. - * @param targetWidth Desired output width, or 0 to use the image width. - * @param targetHeight Desired output height, or 0 to use the image height. + * @param targetWidth Desired output width when scaling; ignored unless {@code targetHeight} is + * also positive. + * @param targetHeight Desired output height when scaling; ignored unless {@code targetWidth} is + * also positive. * @param threads Number of threads to be used for the AVIF decode. * @return a HardwareBuffer on success, or null on failure. */ @@ -61,6 +63,63 @@ public static HardwareBuffer decodeToHardwareBuffer(ByteBuffer encoded, int leng return decodeToHardwareBuffer(encoded, length, 0, 0, threads); } + /** + * Decodes the next frame of an animated AVIF into an {@link HardwareBuffer}. + * + *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. + * Otherwise the cropped image dimensions are used. + * + * @param decoder A live {@link AvifDecoder} instance created via {@link AvifDecoder#create}. Do + * not call {@link AvifDecoder#release()} until this method returns. + */ + @Nullable + public static HardwareBuffer nextFrameHardwareBuffer( + AvifDecoder decoder, int targetWidth, int targetHeight) { + if (decoder == null || !decoder.isAlive()) { + return null; + } + return nextFrameHardwareBufferNative( + decoder.getNativeDecoderHandle(), targetWidth, targetHeight); + } + + /** Decodes the next frame at the cropped image dimensions. */ + @Nullable + public static HardwareBuffer nextFrameHardwareBuffer(AvifDecoder decoder) { + return nextFrameHardwareBuffer(decoder, 0, 0); + } + + /** + * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. + * + *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. + * Otherwise the cropped image dimensions are used. + * + * @param decoder A live {@link AvifDecoder} instance created via {@link AvifDecoder#create}. Do + * not call {@link AvifDecoder#release()} until this method returns. + * @param n The zero-based index of the frame to be decoded. + */ + @Nullable + public static HardwareBuffer nthFrameHardwareBuffer( + AvifDecoder decoder, int n, int targetWidth, int targetHeight) { + if (decoder == null || !decoder.isAlive()) { + return null; + } + return nthFrameHardwareBufferNative( + decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight); + } + + /** Decodes the nth frame at the cropped image dimensions. */ + @Nullable + public static HardwareBuffer nthFrameHardwareBuffer(AvifDecoder decoder, int n) { + return nthFrameHardwareBuffer(decoder, n, 0, 0); + } + private static native HardwareBuffer decodeToHardwareBufferNative( ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads); + + private static native HardwareBuffer nextFrameHardwareBufferNative( + long nativeDecoderHandle, int targetWidth, int targetHeight); + + private static native HardwareBuffer nthFrameHardwareBufferNative( + long nativeDecoderHandle, int n, int targetWidth, int targetHeight); } diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index fad1d67f46..a463322b4d 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -469,6 +469,36 @@ jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height); } +jobject NextFrameToHardwareBuffer(JNIEnv* const env, + AvifDecoderWrapper* const decoder, + int target_width, int target_height) { + const avifResult decode_result = avifDecoderNextImage(decoder->decoder); + if (decode_result != AVIF_RESULT_OK) { + LOGE("Failed to decode AVIF image. Status: %d", decode_result); + return nullptr; + } + uint32_t dst_width = 0; + uint32_t dst_height = 0; + GetTargetDimensions(decoder, target_width, target_height, &dst_width, + &dst_height); + return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); +} + +jobject NthFrameToHardwareBuffer(JNIEnv* const env, + AvifDecoderWrapper* const decoder, uint32_t n, + int target_width, int target_height) { + const avifResult decode_result = avifDecoderNthImage(decoder->decoder, n); + if (decode_result != AVIF_RESULT_OK) { + LOGE("Failed to decode AVIF image. Status: %d", decode_result); + return nullptr; + } + uint32_t dst_width = 0; + uint32_t dst_height = 0; + GetTargetDimensions(decoder, target_width, target_height, &dst_width, + &dst_height); + return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); +} + avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, jobject bitmap) { avifResult res = avifDecoderNextImage(decoder->decoder); @@ -479,6 +509,16 @@ avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, return AvifImageToBitmap(env, decoder, bitmap); } +avifResult DecodeNthImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, + uint32_t n, jobject bitmap) { + avifResult res = avifDecoderNthImage(decoder->decoder, n); + if (res != AVIF_RESULT_OK) { + LOGE("Failed to decode AVIF image. Status: %d", res); + return res; + } + return AvifImageToBitmap(env, decoder, bitmap); +} + } // namespace jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { @@ -558,6 +598,95 @@ FUNC(jboolean, decode, jobject encoded, int length, jobject bitmap, return DecodeNextImage(env, &decoder, bitmap) == AVIF_RESULT_OK; } +FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) { + const uint8_t* buffer = nullptr; + size_t size = 0; + if (!ValidateDirectBuffer(env, encoded, length, &buffer, &size)) { + return 0; + } + std::unique_ptr decoder(new (std::nothrow) + AvifDecoderWrapper()); + if (decoder == nullptr) { + return 0; + } + if (!CreateDecoderAndParse(decoder.get(), buffer, size, + getThreadCount(threads))) { + return 0; + } + FIND_CLASS(avif_decoder_class, "org/aomedia/avif/android/AvifDecoder", 0); + GET_FIELD_ID(width_id, avif_decoder_class, "width", "I", 0); + GET_FIELD_ID(height_id, avif_decoder_class, "height", "I", 0); + GET_FIELD_ID(depth_id, avif_decoder_class, "depth", "I", 0); + GET_FIELD_ID(alpha_present_id, avif_decoder_class, "alphaPresent", "Z", 0); + GET_FIELD_ID(frame_count_id, avif_decoder_class, "frameCount", "I", 0); + GET_FIELD_ID(repetition_count_id, avif_decoder_class, "repetitionCount", "I", + 0); + GET_FIELD_ID(frame_durations_id, avif_decoder_class, "frameDurations", "[D", + 0); + env->SetIntField(thiz, width_id, decoder->crop.width); + CHECK_EXCEPTION(0); + env->SetIntField(thiz, height_id, decoder->crop.height); + CHECK_EXCEPTION(0); + env->SetIntField(thiz, depth_id, decoder->decoder->image->depth); + CHECK_EXCEPTION(0); + env->SetBooleanField(thiz, alpha_present_id, decoder->decoder->alphaPresent); + CHECK_EXCEPTION(0); + env->SetIntField(thiz, repetition_count_id, + decoder->decoder->repetitionCount); + CHECK_EXCEPTION(0); + const int frameCount = decoder->decoder->imageCount; + env->SetIntField(thiz, frame_count_id, frameCount); + CHECK_EXCEPTION(0); + std::unique_ptr native_durations( + new (std::nothrow) double[frameCount]); + if (native_durations == nullptr) { + return 0; + } + for (int i = 0; i < frameCount; ++i) { + avifImageTiming timing; + if (avifDecoderNthImageTiming(decoder->decoder, i, &timing) != + AVIF_RESULT_OK) { + return 0; + } + native_durations[i] = timing.duration; + } + jdoubleArray durations = env->NewDoubleArray(frameCount); + if (durations == nullptr) { + return 0; + } + env->SetDoubleArrayRegion(durations, /*start=*/0, frameCount, + native_durations.get()); + CHECK_EXCEPTION(0); + env->SetObjectField(thiz, frame_durations_id, durations); + CHECK_EXCEPTION(0); + return reinterpret_cast(decoder.release()); +} + +#undef GET_FIELD_ID +#undef FIND_CLASS +#undef CHECK_EXCEPTION + +FUNC(jint, nextFrame, jlong jdecoder, jobject bitmap) { + IGNORE_UNUSED_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + return DecodeNextImage(env, decoder, bitmap); +} + +FUNC(jint, nextFrameIndex, jlong jdecoder) { + IGNORE_UNUSED_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + return decoder->decoder->imageIndex + 1; +} + +FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { + IGNORE_UNUSED_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + return DecodeNthImage(env, decoder, n, bitmap); +} + HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, jint target_width, jint target_height, jint threads) { IGNORE_UNUSED_HW_JNI_PARAMETERS; @@ -565,6 +694,29 @@ HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, target_height, threads); } +HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_width, + jint target_height) { + IGNORE_UNUSED_HW_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + if (decoder == nullptr) { + return nullptr; + } + return NextFrameToHardwareBuffer(env, decoder, target_width, target_height); +} + +HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, + jint target_width, jint target_height) { + IGNORE_UNUSED_HW_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + if (decoder == nullptr) { + return nullptr; + } + return NthFrameToHardwareBuffer(env, decoder, static_cast(n), + target_width, target_height); +} + FUNC(jstring, resultToString, jint result) { IGNORE_UNUSED_JNI_PARAMETERS; return env->NewStringUTF(avifResultToString(static_cast(result))); @@ -586,3 +738,10 @@ FUNC(jstring, versionString) { avifVersion(), codec_versions, libyuv_version); return env->NewStringUTF(version_string); } + +FUNC(void, destroyDecoder, jlong jdecoder) { + IGNORE_UNUSED_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + delete decoder; +} From 824ed9cdf963872b863b9d5c919f17cbedba1882 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sun, 12 Jul 2026 00:13:09 +0900 Subject: [PATCH 06/25] =?UTF-8?q?Fork=E5=B7=AE=E5=88=86=E3=81=8C=E5=B0=8F?= =?UTF-8?q?=E3=81=95=E3=81=8F=E3=81=AA=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/jni/libavif_jni.cc | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index a463322b4d..9038dc8d65 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -42,8 +42,8 @@ ##__VA_ARGS__) #define IGNORE_UNUSED_JNI_PARAMETERS \ - (void)env; \ - (void)thiz + (void) env; \ + (void) thiz #define IGNORE_UNUSED_HW_JNI_PARAMETERS \ (void)env; \ @@ -129,6 +129,9 @@ const HardwareBufferApi& GetHardwareBufferApi() { return api; } +int getThreadCount(int threads); +bool JniExceptionCheck(JNIEnv* env); + // RAII wrapper class that properly frees the decoder related objects on // destruction. struct AvifDecoderWrapper { @@ -179,31 +182,6 @@ bool ValidateDirectBuffer(JNIEnv* env, jobject encoded, jint length, return true; } -int getThreadCount(int threads) { - if (threads < 0) { - return android_getCpuCount(); - } - if (threads == 0) { - // Empirically, on Android devices with more than 1 core, decoding with 2 - // threads is almost always better than using as many threads as CPU cores. - return std::min(android_getCpuCount(), 2); - } - return threads; -} - -// Checks if there is a pending JNI exception that will be thrown when the -// control returns to the java layer. If there is none, it will return false. If -// there is one, then it will clear the pending exception and return true. -// Whenever this function returns true, the caller should treat it as a fatal -// error and return with a failure status as early as possible. -bool JniExceptionCheck(JNIEnv* env) { - if (!env->ExceptionCheck()) { - return false; - } - env->ExceptionClear(); - return true; -} - bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, const uint8_t* const buffer, size_t length, int threads) { @@ -519,6 +497,31 @@ avifResult DecodeNthImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, return AvifImageToBitmap(env, decoder, bitmap); } +int getThreadCount(int threads) { + if (threads < 0) { + return android_getCpuCount(); + } + if (threads == 0) { + // Empirically, on Android devices with more than 1 core, decoding with 2 + // threads is almost always better than using as many threads as CPU cores. + return std::min(android_getCpuCount(), 2); + } + return threads; +} + +// Checks if there is a pending JNI exception that will be thrown when the +// control returns to the java layer. If there is none, it will return false. If +// there is one, then it will clear the pending exception and return true. +// Whenever this function returns true, the caller should treat it as a fatal +// error and return with a failure status as early as possible. +bool JniExceptionCheck(JNIEnv* env) { + if (!env->ExceptionCheck()) { + return false; + } + env->ExceptionClear(); + return true; +} + } // namespace jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) { @@ -637,6 +640,8 @@ FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) { const int frameCount = decoder->decoder->imageCount; env->SetIntField(thiz, frame_count_id, frameCount); CHECK_EXCEPTION(0); + // This native array is needed because setting one element at a time to a Java + // array from the JNI layer is inefficient. std::unique_ptr native_durations( new (std::nothrow) double[frameCount]); if (native_durations == nullptr) { From dbee1dd52c22b5d01f5da6386e0391d45f2100f8 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sun, 12 Jul 2026 16:26:05 +0900 Subject: [PATCH 07/25] =?UTF-8?q?=E3=83=91=E3=82=B9=E3=81=8C=E8=A7=A3?= =?UTF-8?q?=E6=B1=BA=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F?= =?UTF-8?q?=E3=81=AE=E3=81=A7=E6=9B=B8=E3=81=8D=E6=96=B9=E5=A4=89=E3=81=88?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/dav1d_android.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/dav1d_android.sh b/ext/dav1d_android.sh index d31faa2446..c564321701 100755 --- a/ext/dav1d_android.sh +++ b/ext/dav1d_android.sh @@ -34,7 +34,7 @@ ARCH_LIST=("arm" "aarch64" "x86" "x86_64") for i in "${!ABI_LIST[@]}"; do abi="${ABI_LIST[i]}" PATH=$PATH:${android_bin} meson setup --default-library=static --buildtype release \ - --cross-file="../../package/crossfiles/${ARCH_LIST[i]}-android.meson" \ + --cross-file="dav1d/package/crossfiles/${ARCH_LIST[i]}-android.meson" \ -Dbitdepths=8 -Denable_tools=false -Denable_tests=false "dav1d/build/${abi}" dav1d PATH=$PATH:${android_bin} meson compile -C "dav1d/build/${abi}" done From 63e4f0b35d1eb1f4be14101917d181e16f0a634d Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Mon, 13 Jul 2026 10:02:50 +0900 Subject: [PATCH 08/25] =?UTF-8?q?AHardwareBuffer=5FtoHardwareBuffer=20?= =?UTF-8?q?=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF=E5=85=88=E3=81=AE?= =?UTF-8?q?=E8=AA=BF=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/jni/libavif_jni.cc | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 9038dc8d65..6f10ad1884 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -60,7 +60,8 @@ using AHardwareBuffer_release_fn = void (*)(AHardwareBuffer*); using AHardwareBuffer_toHardwareBuffer_fn = jobject (*)(JNIEnv*, AHardwareBuffer*); struct HardwareBufferApi { - void* library = nullptr; + void* buffer_library = nullptr; + void* android_library = nullptr; AHardwareBuffer_allocate_fn allocate = nullptr; AHardwareBuffer_describe_fn describe = nullptr; AHardwareBuffer_lock_fn lock = nullptr; @@ -70,6 +71,29 @@ struct HardwareBufferApi { bool available = false; }; +void CloseHardwareBufferLibraries(HardwareBufferApi* api) { + if (api->buffer_library != nullptr) { + dlclose(api->buffer_library); + api->buffer_library = nullptr; + } + if (api->android_library != nullptr) { + dlclose(api->android_library); + api->android_library = nullptr; + } +} + +void* LoadSymbolFromLibrary(void* library, const char* symbol) { + if (library == nullptr) { + return nullptr; + } + dlerror(); + void* symbol_address = dlsym(library, symbol); + if (symbol_address == nullptr) { + LOGE("Failed to dlsym %s: %s.", symbol, dlerror()); + } + return symbol_address; +} + int GetDeviceApiLevel() { if (android_get_device_api_level != nullptr) { const int api_level = android_get_device_api_level(); @@ -91,34 +115,46 @@ void LoadHardwareBufferApi(HardwareBufferApi* api) { return; } - // All AHardwareBuffer_* symbols used here (including the JNI conversion - // helper) live in libnativewindow.so. Load them from a single library so - // symbol resolution stays consistent across API levels. - api->library = dlopen("libnativewindow.so", RTLD_NOW); - if (api->library == nullptr) { - LOGE("Failed to dlopen libnativewindow.so: %s.", dlerror()); + // Core AHardwareBuffer symbols are exported from libnativewindow.so and/or + // libandroid.so depending on the device. The JNI conversion helper + // AHardwareBuffer_toHardwareBuffer is exported from libandroid.so. + api->buffer_library = dlopen("libnativewindow.so", RTLD_NOW); + api->android_library = dlopen("libandroid.so", RTLD_NOW); + if (api->android_library == nullptr) { + LOGE("Failed to dlopen libandroid.so: %s.", dlerror()); + CloseHardwareBufferLibraries(api); return; } -#define LOAD_SYMBOL(field, symbol) \ - api->field = reinterpret_castfield)>(dlsym(api->library, \ - symbol)); \ - if (api->field == nullptr) { \ - LOGE("Failed to dlsym %s: %s.", symbol, dlerror()); \ - dlclose(api->library); \ - api->library = nullptr; \ - return; \ + auto load_buffer_symbol = [&](const char* symbol) -> void* { + void* symbol_address = LoadSymbolFromLibrary(api->buffer_library, symbol); + if (symbol_address == nullptr) { + symbol_address = LoadSymbolFromLibrary(api->android_library, symbol); + } + return symbol_address; + }; + + api->allocate = reinterpret_cast( + load_buffer_symbol("AHardwareBuffer_allocate")); + api->describe = reinterpret_cast( + load_buffer_symbol("AHardwareBuffer_describe")); + api->lock = reinterpret_cast( + load_buffer_symbol("AHardwareBuffer_lock")); + api->unlock = reinterpret_cast( + load_buffer_symbol("AHardwareBuffer_unlock")); + api->release = reinterpret_cast( + load_buffer_symbol("AHardwareBuffer_release")); + api->to_hardware_buffer = + reinterpret_cast(LoadSymbolFromLibrary( + api->android_library, "AHardwareBuffer_toHardwareBuffer")); + + if (api->allocate == nullptr || api->describe == nullptr || + api->lock == nullptr || api->unlock == nullptr || + api->release == nullptr || api->to_hardware_buffer == nullptr) { + CloseHardwareBufferLibraries(api); + return; } - LOAD_SYMBOL(allocate, "AHardwareBuffer_allocate"); - LOAD_SYMBOL(describe, "AHardwareBuffer_describe"); - LOAD_SYMBOL(lock, "AHardwareBuffer_lock"); - LOAD_SYMBOL(unlock, "AHardwareBuffer_unlock"); - LOAD_SYMBOL(release, "AHardwareBuffer_release"); - LOAD_SYMBOL(to_hardware_buffer, "AHardwareBuffer_toHardwareBuffer"); - -#undef LOAD_SYMBOL - api->available = true; } @@ -376,6 +412,8 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, uint32_t dst_width, uint32_t dst_height) { const HardwareBufferApi& hw_api = GetHardwareBufferApi(); if (!hw_api.available) { + LOGE("HardwareBuffer API is unavailable. Check earlier avif_jni logs for " + "dlopen/dlsym errors."); return nullptr; } From bb619ee41fdd8f72fc3bfa12880733747796f040 Mon Sep 17 00:00:00 2001 From: k2w4t4h Date: Mon, 13 Jul 2026 14:25:38 +0900 Subject: [PATCH 09/25] =?UTF-8?q?R8=E5=AF=BE=E5=BF=9C=E3=81=A8=E3=82=8A?= =?UTF-8?q?=E3=81=82=E3=81=88=E3=81=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/README.md | 51 +++++ android_jni/avifandroidjni/build.gradle | 2 +- .../src/androidTest/assets/README.md | 6 + .../avif/android/AvifHardwareDecoder.java | 78 ++++++- .../src/main/jni/libavif_jni.cc | 196 +++++++++++++++--- 5 files changed, 290 insertions(+), 43 deletions(-) diff --git a/android_jni/README.md b/android_jni/README.md index d57371c4b5..4f9c9a18de 100644 --- a/android_jni/README.md +++ b/android_jni/README.md @@ -116,3 +116,54 @@ To build the project from within Android Studio, follow the all the steps from t Maven hosted version of libavif can be found here: https://repo1.maven.org/maven2/org/aomedia/avif/android/avif/ + +## Hardware Buffer Decoding + +Use `AvifHardwareDecoder` on API 29+ to decode directly into `HardwareBuffer` objects suitable +for GPU sampling (for example via `Bitmap.wrapHardwareBuffer`). + +By default, output uses `RGBA_8888`. Pass `allowR8 = true` to opt in to single-channel `R_8` +output when **all** of the following hold: + +* The decoded image is 8-bit monochrome (`YUV400`) with no alpha plane. +* The device runs API 35 or newer. +* `AHardwareBuffer_isSupported` reports that `R_8` allocation is available for the requested + dimensions and usage (`CPU_WRITE_RARELY | GPU_SAMPLED_IMAGE`). + +If any condition fails, or if `AHardwareBuffer_allocate` fails for `R_8`, the decoder falls back +to `RGBA_8888`. + +### Range conversion (R_8 path) + +Monochrome `R_8` output copies the Y plane directly instead of running the full YUV→RGB matrix. +Limited-range Y values (16–235) are expanded to full-range 0–255 via a per-sample LUT; full-range +Y is copied as-is. Only the range is transformed—color matrix coefficients (BT.601/709) do not +apply to this single-channel path. + +### Display responsibility + +An `R_8` buffer stores luminance in one channel. Wrapping or drawing it without a color transform +typically appears as red-tinted intensity. Callers must apply a `ColorMatrixColorFilter` (or +equivalent) when presenting the buffer as grayscale or RGB. + +### Example + +```java +if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + HardwareBuffer buffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + encoded, encoded.remaining(), 0, 0, threads, /* allowR8= */ true); + if (buffer != null) { + if (Build.VERSION.SDK_INT >= 35 + && buffer.getFormat() == HardwareBuffer.R_8) { + // Apply a ColorMatrixColorFilter before drawing. + } + buffer.close(); + } +} +``` + +Range-specific monochrome test assets (`mono_8bpc_limited.avif`, `mono_8bpc_full.avif`) can be +generated with +[`generate_mono_test_assets.sh`](avifandroidjni/src/androidTest/assets/generate_mono_test_assets.sh). + diff --git a/android_jni/avifandroidjni/build.gradle b/android_jni/avifandroidjni/build.gradle index da7e3e8bdc..f037f6b53e 100644 --- a/android_jni/avifandroidjni/build.gradle +++ b/android_jni/avifandroidjni/build.gradle @@ -4,7 +4,7 @@ plugins { android { namespace 'org.aomedia.avif.android' - compileSdk 31 + compileSdk 35 ndkVersion "25.2.9519653" defaultConfig { diff --git a/android_jni/avifandroidjni/src/androidTest/assets/README.md b/android_jni/avifandroidjni/src/androidTest/assets/README.md index f9463b98e8..b998d2aed2 100644 --- a/android_jni/avifandroidjni/src/androidTest/assets/README.md +++ b/android_jni/avifandroidjni/src/androidTest/assets/README.md @@ -22,3 +22,9 @@ Test files for animated AVIF decoding. alpha_video.avif is patched at 0-indexed byte #259 replaced with 0x04 instead of 0x00 as [0x00 is an invalid item_id for the iref box](https://github.com/AOMediaCodec/av1-avif/issues/217). + +### mono_8bpc_limited.avif and mono_8bpc_full.avif + +8-bit monochrome (`YUV400`) assets with explicit limited/full range for `R_8` LUT tests. Generate +with [generate_mono_test_assets.sh](generate_mono_test_assets.sh) (requires `avifenc` and +ImageMagick). diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java index 5af27a51a0..899418fc6a 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -50,7 +50,36 @@ private AvifHardwareDecoder() {} @Nullable public static HardwareBuffer decodeToHardwareBuffer( ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads) { - return decodeToHardwareBufferNative(encoded, length, targetWidth, targetHeight, threads); + return decodeToHardwareBuffer(encoded, length, targetWidth, targetHeight, threads, false); + } + + /** + * Decodes the AVIF image into an {@link HardwareBuffer}. + * + *

When {@code allowR8} is {@code true} and the image is 8-bit monochrome (YUV400) without + * alpha on a device running API 35+ that supports {@link HardwareBuffer#R_8 R_8} allocation, the + * returned buffer's {@link HardwareBuffer#getFormat()} will be {@link HardwareBuffer#R_8} (56). + * Otherwise the format is {@link HardwareBuffer#RGBA_8888} as in the overload without {@code + * allowR8}. + * + *

An {@code R_8} buffer holds a single luminance channel. Drawing it directly (for example via + * {@link android.graphics.Bitmap#wrapHardwareBuffer}) shows red-tinted intensity; callers must + * apply a {@link android.graphics.ColorMatrixColorFilter} or equivalent color transform for + * correct grayscale or RGB display. + * + * @param allowR8 When {@code true}, opt in to {@code R_8} output for eligible monochrome images. + * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) + */ + @Nullable + public static HardwareBuffer decodeToHardwareBuffer( + ByteBuffer encoded, + int length, + int targetWidth, + int targetHeight, + int threads, + boolean allowR8) { + return decodeToHardwareBufferNative( + encoded, length, targetWidth, targetHeight, threads, allowR8); } /** @@ -60,7 +89,7 @@ public static HardwareBuffer decodeToHardwareBuffer( */ @Nullable public static HardwareBuffer decodeToHardwareBuffer(ByteBuffer encoded, int length, int threads) { - return decodeToHardwareBuffer(encoded, length, 0, 0, threads); + return decodeToHardwareBuffer(encoded, length, 0, 0, threads, false); } /** @@ -75,17 +104,30 @@ public static HardwareBuffer decodeToHardwareBuffer(ByteBuffer encoded, int leng @Nullable public static HardwareBuffer nextFrameHardwareBuffer( AvifDecoder decoder, int targetWidth, int targetHeight) { + return nextFrameHardwareBuffer(decoder, targetWidth, targetHeight, false); + } + + /** + * Decodes the next frame of an animated AVIF into an {@link HardwareBuffer}. + * + * @param allowR8 When {@code true}, opt in to {@code R_8} output for eligible monochrome frames. + * See {@link #decodeToHardwareBuffer(ByteBuffer, int, int, int, int, boolean)} for format + * selection rules and display responsibilities. + */ + @Nullable + public static HardwareBuffer nextFrameHardwareBuffer( + AvifDecoder decoder, int targetWidth, int targetHeight, boolean allowR8) { if (decoder == null || !decoder.isAlive()) { return null; } return nextFrameHardwareBufferNative( - decoder.getNativeDecoderHandle(), targetWidth, targetHeight); + decoder.getNativeDecoderHandle(), targetWidth, targetHeight, allowR8); } /** Decodes the next frame at the cropped image dimensions. */ @Nullable public static HardwareBuffer nextFrameHardwareBuffer(AvifDecoder decoder) { - return nextFrameHardwareBuffer(decoder, 0, 0); + return nextFrameHardwareBuffer(decoder, 0, 0, false); } /** @@ -101,25 +143,43 @@ public static HardwareBuffer nextFrameHardwareBuffer(AvifDecoder decoder) { @Nullable public static HardwareBuffer nthFrameHardwareBuffer( AvifDecoder decoder, int n, int targetWidth, int targetHeight) { + return nthFrameHardwareBuffer(decoder, n, targetWidth, targetHeight, false); + } + + /** + * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. + * + * @param allowR8 When {@code true}, opt in to {@code R_8} output for eligible monochrome frames. + * See {@link #decodeToHardwareBuffer(ByteBuffer, int, int, int, int, boolean)} for format + * selection rules and display responsibilities. + */ + @Nullable + public static HardwareBuffer nthFrameHardwareBuffer( + AvifDecoder decoder, int n, int targetWidth, int targetHeight, boolean allowR8) { if (decoder == null || !decoder.isAlive()) { return null; } return nthFrameHardwareBufferNative( - decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight); + decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight, allowR8); } /** Decodes the nth frame at the cropped image dimensions. */ @Nullable public static HardwareBuffer nthFrameHardwareBuffer(AvifDecoder decoder, int n) { - return nthFrameHardwareBuffer(decoder, n, 0, 0); + return nthFrameHardwareBuffer(decoder, n, 0, 0, false); } private static native HardwareBuffer decodeToHardwareBufferNative( - ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads); + ByteBuffer encoded, + int length, + int targetWidth, + int targetHeight, + int threads, + boolean allowR8); private static native HardwareBuffer nextFrameHardwareBufferNative( - long nativeDecoderHandle, int targetWidth, int targetHeight); + long nativeDecoderHandle, int targetWidth, int targetHeight, boolean allowR8); private static native HardwareBuffer nthFrameHardwareBufferNative( - long nativeDecoderHandle, int n, int targetWidth, int targetHeight); + long nativeDecoderHandle, int n, int targetWidth, int targetHeight, boolean allowR8); } diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 6f10ad1884..6cc564fa73 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -13,12 +13,17 @@ #include #include #include +#include #include #include #include #include "avif/avif.h" +#ifndef AHARDWAREBUFFER_FORMAT_R8_UNORM +#define AHARDWAREBUFFER_FORMAT_R8_UNORM 0x38 // = 56, API 35 +#endif + #define LOG_TAG "avif_jni" #define LOGE(...) \ ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) @@ -53,6 +58,7 @@ namespace { using AHardwareBuffer_allocate_fn = int (*)(const AHardwareBuffer_Desc*, AHardwareBuffer**); using AHardwareBuffer_describe_fn = void (*)(const AHardwareBuffer*, AHardwareBuffer_Desc*); +using AHardwareBuffer_isSupported_fn = bool (*)(const AHardwareBuffer_Desc*); using AHardwareBuffer_lock_fn = int (*)(AHardwareBuffer*, uint64_t, int32_t, const ARect*, void**); using AHardwareBuffer_unlock_fn = int (*)(AHardwareBuffer*, int32_t*); @@ -64,6 +70,7 @@ struct HardwareBufferApi { void* android_library = nullptr; AHardwareBuffer_allocate_fn allocate = nullptr; AHardwareBuffer_describe_fn describe = nullptr; + AHardwareBuffer_isSupported_fn is_supported = nullptr; AHardwareBuffer_lock_fn lock = nullptr; AHardwareBuffer_unlock_fn unlock = nullptr; AHardwareBuffer_release_fn release = nullptr; @@ -138,6 +145,8 @@ void LoadHardwareBufferApi(HardwareBufferApi* api) { load_buffer_symbol("AHardwareBuffer_allocate")); api->describe = reinterpret_cast( load_buffer_symbol("AHardwareBuffer_describe")); + api->is_supported = reinterpret_cast( + load_buffer_symbol("AHardwareBuffer_isSupported")); api->lock = reinterpret_cast( load_buffer_symbol("AHardwareBuffer_lock")); api->unlock = reinterpret_cast( @@ -335,6 +344,25 @@ avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, return image; } +avifResult AvifImageToRGBBufferFromImage(avifImage* image, void* pixels, + size_t row_bytes) { + avifRGBImage rgb_image; + avifRGBImageSetDefaults(&rgb_image, image); + rgb_image.format = AVIF_RGB_FORMAT_RGBA; + rgb_image.depth = 8; + rgb_image.pixels = static_cast(pixels); + rgb_image.rowBytes = row_bytes; + // Android always sees the Bitmaps as premultiplied with alpha when it renders + // them: + // https://developer.android.com/reference/android/graphics/Bitmap#setPremultiplied(boolean) + rgb_image.alphaPremultiplied = AVIF_TRUE; + const avifResult res = avifImageYUVToRGB(image, &rgb_image); + if (res != AVIF_RESULT_OK) { + LOGE("Failed to convert YUV Pixels to RGB. Status: %d", res); + } + return res; +} + avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, uint32_t dst_width, uint32_t dst_height, size_t row_bytes, avifRGBFormat format, @@ -350,6 +378,10 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, return res; } + if (format == AVIF_RGB_FORMAT_RGBA && depth == 8 && !is_float) { + return AvifImageToRGBBufferFromImage(image, pixels, row_bytes); + } + avifRGBImage rgb_image; avifRGBImageSetDefaults(&rgb_image, image); rgb_image.format = format; @@ -357,9 +389,6 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, rgb_image.isFloat = is_float; rgb_image.pixels = static_cast(pixels); rgb_image.rowBytes = row_bytes; - // Android always sees the Bitmaps as premultiplied with alpha when it renders - // them: - // https://developer.android.com/reference/android/graphics/Bitmap#setPremultiplied(boolean) rgb_image.alphaPremultiplied = AVIF_TRUE; res = avifImageYUVToRGB(image, &rgb_image); if (res != AVIF_RESULT_OK) { @@ -368,6 +397,74 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, return res; } +avifResult AvifImageToGrayBuffer(const HardwareBufferApi& hw_api, + AHardwareBuffer* hw_buffer, + avifImage* image) { + AHardwareBuffer_Desc desc; + hw_api.describe(hw_buffer, &desc); + + void* pixels = nullptr; + if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, + &pixels) != 0) { + LOGE("AHardwareBuffer_lock failed."); + return AVIF_RESULT_UNKNOWN_ERROR; + } + + uint8_t* dst = static_cast(pixels); + const uint8_t* src_y = image->yuvPlanes[AVIF_CHAN_Y]; + const uint32_t width = image->width; + const uint32_t height = image->height; + const size_t src_row_bytes = image->yuvRowBytes[AVIF_CHAN_Y]; + + if (image->yuvRange == AVIF_RANGE_LIMITED) { + uint8_t lut[256]; + for (int v = 0; v < 256; ++v) { + const int e = ((v - 16) * 255 + 109) / 219; + lut[v] = static_cast(std::clamp(e, 0, 255)); + } + for (uint32_t y = 0; y < height; ++y) { + const uint8_t* src_row = src_y + y * src_row_bytes; + uint8_t* dst_row = dst + y * desc.stride; + for (uint32_t x = 0; x < width; ++x) { + dst_row[x] = lut[src_row[x]]; + } + } + } else { + for (uint32_t y = 0; y < height; ++y) { + std::memcpy(dst + y * desc.stride, src_y + y * src_row_bytes, width); + } + } + + hw_api.unlock(hw_buffer, nullptr); + return AVIF_RESULT_OK; +} + +bool ShouldUseR8Output(avifImage* image, bool allow_r8, + const HardwareBufferApi& hw_api, + AHardwareBuffer_Desc* desc) { + if (!allow_r8) { + return false; + } + if (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) { + return false; + } + if (image->alphaPlane != nullptr) { + return false; + } + if (image->depth != 8) { + return false; + } + if (GetDeviceApiLevel() < 35) { + return false; + } + + desc->format = AHARDWAREBUFFER_FORMAT_R8_UNORM; + if (hw_api.is_supported != nullptr && !hw_api.is_supported(desc)) { + return false; + } + return true; +} + avifResult AvifImageToBitmap(JNIEnv* const env, AvifDecoderWrapper* const decoder, jobject bitmap) { @@ -409,7 +506,8 @@ avifResult AvifImageToBitmap(JNIEnv* const env, jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, - uint32_t dst_width, uint32_t dst_height) { + uint32_t dst_width, uint32_t dst_height, + bool allow_r8) { const HardwareBufferApi& hw_api = GetHardwareBufferApi(); if (!hw_api.available) { LOGE("HardwareBuffer API is unavailable. Check earlier avif_jni logs for " @@ -417,35 +515,60 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, return nullptr; } + avifResult res; + std::unique_ptr cropped_image( + nullptr, avifImageDestroy); + std::unique_ptr image_copy( + nullptr, avifImageDestroy); + avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, + cropped_image, image_copy); + if (image == nullptr) { + return nullptr; + } + AHardwareBuffer_Desc desc = {}; desc.width = dst_width; desc.height = dst_height; desc.layers = 1; - desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; + bool use_r8 = ShouldUseR8Output(image, allow_r8, hw_api, &desc); + AHardwareBuffer* hw_buffer = nullptr; - if (hw_api.allocate(&desc, &hw_buffer) != 0) { - LOGE("AHardwareBuffer_allocate failed."); - return nullptr; + if (use_r8) { + desc.format = AHARDWAREBUFFER_FORMAT_R8_UNORM; + if (hw_api.allocate(&desc, &hw_buffer) != 0) { + LOGE("AHardwareBuffer_allocate failed for R8; falling back to RGBA."); + use_r8 = false; + hw_buffer = nullptr; + } + } + if (!use_r8) { + desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + if (hw_api.allocate(&desc, &hw_buffer) != 0) { + LOGE("AHardwareBuffer_allocate failed."); + return nullptr; + } } hw_api.describe(hw_buffer, &desc); - void* pixels = nullptr; - if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, - &pixels) != 0) { - LOGE("AHardwareBuffer_lock failed."); - hw_api.release(hw_buffer); - return nullptr; + avifResult fill_res; + if (use_r8) { + fill_res = AvifImageToGrayBuffer(hw_api, hw_buffer, image); + } else { + void* pixels = nullptr; + if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, + &pixels) != 0) { + LOGE("AHardwareBuffer_lock failed."); + hw_api.release(hw_buffer); + return nullptr; + } + fill_res = AvifImageToRGBBufferFromImage( + image, pixels, static_cast(desc.stride) * 4); + hw_api.unlock(hw_buffer, nullptr); } - - const avifResult res = AvifImageToRGBBuffer( - decoder, pixels, dst_width, dst_height, - static_cast(desc.stride) * 4, AVIF_RGB_FORMAT_RGBA, 8, - AVIF_FALSE); - hw_api.unlock(hw_buffer, nullptr); - if (res != AVIF_RESULT_OK) { + if (fill_res != AVIF_RESULT_OK) { hw_api.release(hw_buffer); return nullptr; } @@ -463,7 +586,7 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, int target_width, int target_height, - int threads) { + int threads, bool allow_r8) { const uint8_t* buffer = nullptr; size_t size = 0; if (!ValidateDirectBuffer(env, encoded, length, &buffer, &size)) { @@ -482,12 +605,14 @@ jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, LOGE("Failed to decode AVIF image for HardwareBuffer output."); return nullptr; } - return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height); + return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height, + allow_r8); } jobject NextFrameToHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, - int target_width, int target_height) { + int target_width, int target_height, + bool allow_r8) { const avifResult decode_result = avifDecoderNextImage(decoder->decoder); if (decode_result != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", decode_result); @@ -497,12 +622,14 @@ jobject NextFrameToHardwareBuffer(JNIEnv* const env, uint32_t dst_height = 0; GetTargetDimensions(decoder, target_width, target_height, &dst_width, &dst_height); - return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); + return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height, + allow_r8); } jobject NthFrameToHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t n, - int target_width, int target_height) { + int target_width, int target_height, + bool allow_r8) { const avifResult decode_result = avifDecoderNthImage(decoder->decoder, n); if (decode_result != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", decode_result); @@ -512,7 +639,8 @@ jobject NthFrameToHardwareBuffer(JNIEnv* const env, uint32_t dst_height = 0; GetTargetDimensions(decoder, target_width, target_height, &dst_width, &dst_height); - return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height); + return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height, + allow_r8); } avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, @@ -731,25 +859,26 @@ FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { } HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, - jint target_width, jint target_height, jint threads) { + jint target_width, jint target_height, jint threads, jboolean allow_r8) { IGNORE_UNUSED_HW_JNI_PARAMETERS; return DecodeToHardwareBuffer(env, encoded, length, target_width, - target_height, threads); + target_height, threads, allow_r8 != JNI_FALSE); } HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_width, - jint target_height) { + jint target_height, jboolean allow_r8) { IGNORE_UNUSED_HW_JNI_PARAMETERS; AvifDecoderWrapper* const decoder = reinterpret_cast(jdecoder); if (decoder == nullptr) { return nullptr; } - return NextFrameToHardwareBuffer(env, decoder, target_width, target_height); + return NextFrameToHardwareBuffer(env, decoder, target_width, target_height, + allow_r8 != JNI_FALSE); } HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, - jint target_width, jint target_height) { + jint target_width, jint target_height, jboolean allow_r8) { IGNORE_UNUSED_HW_JNI_PARAMETERS; AvifDecoderWrapper* const decoder = reinterpret_cast(jdecoder); @@ -757,7 +886,8 @@ HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, return nullptr; } return NthFrameToHardwareBuffer(env, decoder, static_cast(n), - target_width, target_height); + target_width, target_height, + allow_r8 != JNI_FALSE); } FUNC(jstring, resultToString, jint result) { From 2ee82f19c9efef4245242eff9ba9ba4a9177c865 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Mon, 13 Jul 2026 23:50:21 +0900 Subject: [PATCH 10/25] BugFix --- android_jni/avifandroidjni/build.gradle | 2 +- .../avifandroidjni/src/main/jni/libavif_jni.cc | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/android_jni/avifandroidjni/build.gradle b/android_jni/avifandroidjni/build.gradle index f037f6b53e..da7e3e8bdc 100644 --- a/android_jni/avifandroidjni/build.gradle +++ b/android_jni/avifandroidjni/build.gradle @@ -4,7 +4,7 @@ plugins { android { namespace 'org.aomedia.avif.android' - compileSdk 35 + compileSdk 31 ndkVersion "25.2.9519653" defaultConfig { diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 6cc564fa73..caa20a159f 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -102,11 +102,9 @@ void* LoadSymbolFromLibrary(void* library, const char* symbol) { } int GetDeviceApiLevel() { - if (android_get_device_api_level != nullptr) { - const int api_level = android_get_device_api_level(); - if (api_level > 0) { - return api_level; - } + const int api_level = android_get_device_api_level(); + if (api_level > 0) { + return api_level; } char sdk_version[PROP_VALUE_MAX] = {}; @@ -419,8 +417,13 @@ avifResult AvifImageToGrayBuffer(const HardwareBufferApi& hw_api, if (image->yuvRange == AVIF_RANGE_LIMITED) { uint8_t lut[256]; for (int v = 0; v < 256; ++v) { - const int e = ((v - 16) * 255 + 109) / 219; - lut[v] = static_cast(std::clamp(e, 0, 255)); + int e = ((v - 16) * 255 + 109) / 219; + if (e < 0) { + e = 0; + } else if (e > 255) { + e = 255; + } + lut[v] = static_cast(e); } for (uint32_t y = 0; y < height; ++y) { const uint8_t* src_row = src_y + y * src_row_bytes; From 93a8b38e5da632e00e15affc943ff460a334a804 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Tue, 14 Jul 2026 10:44:30 +0900 Subject: [PATCH 11/25] =?UTF-8?q?RGB565=E3=81=AB=E5=88=87=E6=9B=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/README.md | 50 +++-- .../src/androidTest/assets/README.md | 11 +- .../src/androidTest/assets/gen_mono_ramp.c | 88 ++++++++ .../assets/generate_mono_test_assets.sh | 71 ++++++ .../AvifHardwareDecoderGray565Test.java | 210 ++++++++++++++++++ .../avif/android/AvifHardwareDecoder.java | 49 ++-- .../src/main/jni/libavif_jni.cc | 119 +++++----- 7 files changed, 486 insertions(+), 112 deletions(-) create mode 100644 android_jni/avifandroidjni/src/androidTest/assets/gen_mono_ramp.c create mode 100644 android_jni/avifandroidjni/src/androidTest/assets/generate_mono_test_assets.sh create mode 100644 android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifHardwareDecoderGray565Test.java diff --git a/android_jni/README.md b/android_jni/README.md index 4f9c9a18de..2b1e0bc08c 100644 --- a/android_jni/README.md +++ b/android_jni/README.md @@ -122,29 +122,36 @@ https://repo1.maven.org/maven2/org/aomedia/avif/android/avif/ Use `AvifHardwareDecoder` on API 29+ to decode directly into `HardwareBuffer` objects suitable for GPU sampling (for example via `Bitmap.wrapHardwareBuffer`). -By default, output uses `RGBA_8888`. Pass `allowR8 = true` to opt in to single-channel `R_8` -output when **all** of the following hold: +By default, output uses `RGBA_8888`. Pass `allowGray565 = true` to opt in to **Gray565** packing +(`HardwareBuffer.RGB_565`) when **all** of the following hold: * The decoded image is 8-bit monochrome (`YUV400`) with no alpha plane. -* The device runs API 35 or newer. -* `AHardwareBuffer_isSupported` reports that `R_8` allocation is available for the requested - dimensions and usage (`CPU_WRITE_RARELY | GPU_SAMPLED_IMAGE`). +* `allowGray565` is `true`. -If any condition fails, or if `AHardwareBuffer_allocate` fails for `R_8`, the decoder falls back -to `RGBA_8888`. +If any condition fails, or if `AHardwareBuffer_allocate` fails for `R5G6B5`, the decoder falls +back to `RGBA_8888`. (`R5G6B5` is a universally supported HardwareBuffer format from API 26; the +caller's API 29 gate for `wrapHardwareBuffer` is sufficient—there is no API 35 requirement.) -### Range conversion (R_8 path) +### Gray565 packing -Monochrome `R_8` output copies the Y plane directly instead of running the full YUV→RGB matrix. -Limited-range Y values (16–235) are expanded to full-range 0–255 via a per-sample LUT; full-range -Y is copied as-is. Only the range is transformed—color matrix coefficients (BT.601/709) do not -apply to this single-channel path. +`RGB_565` here is **not** a true color RGB565 image. It is an 8-bit grayscale value packed into +the RGB565 bit fields (R is the most-significant field): -### Display responsibility +* Encoding: `Y = 4 * G6 + (R5 & 3)`, `B5 = 0` + equivalently `pixel = ((Y & 3) << 11) | ((Y >> 2) << 5)`. +* Limited-range Y (16–235) is expanded to 0–255 in the same LUT that builds the packed value; + full-range Y is used as-is. Color matrix coefficients (BT.601/709) do not apply on this path. +* Scaling (when requested) is applied with `avifImageScale` on the YUV image **before** packing. -An `R_8` buffer stores luminance in one channel. Wrapping or drawing it without a color transform -typically appears as red-tinted intensity. Callers must apply a `ColorMatrixColorFilter` (or -equivalent) when presenting the buffer as grayscale or RGB. +### Display responsibility (required) + +Drawing a Gray565 buffer without a restore transform looks like greenish noise, not grayscale. +Callers **must** apply a restore `ColorMatrix` / `ColorMatrixColorFilter` that maps each of R, G, +B to: + +`31/255 · r + 252/255 · g` + +(where `r`/`g` are the 8-bit expanded R/G channels from the RGB565 sample). ### Example @@ -152,18 +159,17 @@ equivalent) when presenting the buffer as grayscale or RGB. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { HardwareBuffer buffer = AvifHardwareDecoder.decodeToHardwareBuffer( - encoded, encoded.remaining(), 0, 0, threads, /* allowR8= */ true); + encoded, encoded.remaining(), 0, 0, threads, /* allowGray565= */ true); if (buffer != null) { - if (Build.VERSION.SDK_INT >= 35 - && buffer.getFormat() == HardwareBuffer.R_8) { - // Apply a ColorMatrixColorFilter before drawing. + if (buffer.getFormat() == HardwareBuffer.RGB_565) { + // Apply the Gray565 restore ColorMatrix before drawing. } buffer.close(); } } ``` -Range-specific monochrome test assets (`mono_8bpc_limited.avif`, `mono_8bpc_full.avif`) can be -generated with +Range-specific monochrome test assets (`mono_8bpc_limited.avif`, `mono_8bpc_full.avif`, +`mono_8bpc_ramp_full.avif`) can be generated with [`generate_mono_test_assets.sh`](avifandroidjni/src/androidTest/assets/generate_mono_test_assets.sh). diff --git a/android_jni/avifandroidjni/src/androidTest/assets/README.md b/android_jni/avifandroidjni/src/androidTest/assets/README.md index b998d2aed2..dd5511a3c2 100644 --- a/android_jni/avifandroidjni/src/androidTest/assets/README.md +++ b/android_jni/avifandroidjni/src/androidTest/assets/README.md @@ -23,8 +23,11 @@ Test files for animated AVIF decoding. alpha_video.avif is patched at 0-indexed byte #259 replaced with 0x04 instead of 0x00 as [0x00 is an invalid item_id for the iref box](https://github.com/AOMediaCodec/av1-avif/issues/217). -### mono_8bpc_limited.avif and mono_8bpc_full.avif +### mono_8bpc_limited.avif, mono_8bpc_full.avif, mono_8bpc_ramp_full.avif -8-bit monochrome (`YUV400`) assets with explicit limited/full range for `R_8` LUT tests. Generate -with [generate_mono_test_assets.sh](generate_mono_test_assets.sh) (requires `avifenc` and -ImageMagick). +8-bit monochrome (`YUV400`) 256×16 ramps whose columns cover every code 0–255, with explicit +limited/full range for Gray565 LUT tests (`mono_8bpc_ramp_full.avif` is a copy of the full-range +file used by the restore ColorMatrix golden test). + +Generate with [generate_mono_test_assets.sh](generate_mono_test_assets.sh) (uses +[gen_mono_ramp.c](gen_mono_ramp.c) against a libavif+aom build, or `avifenc` if available). diff --git a/android_jni/avifandroidjni/src/androidTest/assets/gen_mono_ramp.c b/android_jni/avifandroidjni/src/androidTest/assets/gen_mono_ramp.c new file mode 100644 index 0000000000..137318d37d --- /dev/null +++ b/android_jni/avifandroidjni/src/androidTest/assets/gen_mono_ramp.c @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: BSD-2-Clause +// Helper: encode a 256x16 YUV400 ramp AVIF (limited or full range). +#include "avif/avif.h" + +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 3) { + fprintf(stderr, "Usage: %s \n", argv[0]); + return 1; + } + const int limited = strcmp(argv[1], "limited") == 0; + if (!limited && strcmp(argv[1], "full") != 0) { + fprintf(stderr, "range must be limited or full\n"); + return 1; + } + + const uint32_t width = 256; + const uint32_t height = 16; + avifImage* image = avifImageCreate(width, height, 8, AVIF_PIXEL_FORMAT_YUV400); + if (!image) { + fprintf(stderr, "avifImageCreate failed\n"); + return 1; + } + image->yuvRange = limited ? AVIF_RANGE_LIMITED : AVIF_RANGE_FULL; + image->colorPrimaries = AVIF_COLOR_PRIMARIES_UNSPECIFIED; + image->transferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_UNSPECIFIED; + image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED; + + if (avifImageAllocatePlanes(image, AVIF_PLANES_YUV) != AVIF_RESULT_OK) { + fprintf(stderr, "avifImageAllocatePlanes failed\n"); + avifImageDestroy(image); + return 1; + } + + for (uint32_t y = 0; y < height; ++y) { + uint8_t* row = image->yuvPlanes[AVIF_CHAN_Y] + y * image->yuvRowBytes[AVIF_CHAN_Y]; + for (uint32_t x = 0; x < width; ++x) { + row[x] = (uint8_t)x; + } + } + + avifRWData raw = AVIF_DATA_EMPTY; + avifEncoder* encoder = avifEncoderCreate(); + if (!encoder) { + fprintf(stderr, "avifEncoderCreate failed\n"); + avifImageDestroy(image); + return 1; + } + // Near-lossless so Gray565 golden tests see every code. + encoder->quality = 100; + encoder->qualityAlpha = 100; + encoder->speed = 6; + + avifResult res = avifEncoderAddImage(encoder, image, 1, AVIF_ADD_IMAGE_FLAG_SINGLE); + if (res != AVIF_RESULT_OK) { + fprintf(stderr, "avifEncoderAddImage: %s\n", avifResultToString(res)); + avifEncoderDestroy(encoder); + avifImageDestroy(image); + return 1; + } + res = avifEncoderFinish(encoder, &raw); + if (res != AVIF_RESULT_OK) { + fprintf(stderr, "avifEncoderFinish: %s\n", avifResultToString(res)); + avifEncoderDestroy(encoder); + avifImageDestroy(image); + return 1; + } + + FILE* f = fopen(argv[2], "wb"); + if (!f) { + perror("fopen"); + avifRWDataFree(&raw); + avifEncoderDestroy(encoder); + avifImageDestroy(image); + return 1; + } + fwrite(raw.data, 1, raw.size, f); + fclose(f); + printf("Wrote %s (%zu bytes)\n", argv[2], raw.size); + + avifRWDataFree(&raw); + avifEncoderDestroy(encoder); + avifImageDestroy(image); + return 0; +} diff --git a/android_jni/avifandroidjni/src/androidTest/assets/generate_mono_test_assets.sh b/android_jni/avifandroidjni/src/androidTest/assets/generate_mono_test_assets.sh new file mode 100644 index 0000000000..71a94af7f4 --- /dev/null +++ b/android_jni/avifandroidjni/src/androidTest/assets/generate_mono_test_assets.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Generate Gray565 instrumentation-test AVIF assets. +# +# Preferred path: compile gen_mono_ramp.c against a libavif build with an AV1 +# encoder (aom). Fallback: avifenc if present on PATH. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_DIR="${SCRIPT_DIR}/avif" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../../.." && pwd)" +mkdir -p "${OUT_DIR}" + +generate_with_helper() { + local build_dir="${REPO_ROOT}/build.avifenc" + local helper="${build_dir}/gen_mono_ramp" + if [[ ! -x "${helper}" ]]; then + if [[ ! -f "${build_dir}/libavif_internal.a" ]]; then + echo "Missing ${build_dir}/libavif_internal.a — build libavif with AVIF_CODEC_AOM=LOCAL first." >&2 + return 1 + fi + cc -O2 -I "${REPO_ROOT}/include" \ + "${SCRIPT_DIR}/gen_mono_ramp.c" \ + -o "${helper}" \ + "${build_dir}/libavif_internal.a" \ + "${REPO_ROOT}/ext/aom/build.libavif/libaom.a" \ + -lpthread -lm -ldl + fi + "${helper}" full "${OUT_DIR}/mono_8bpc_full.avif" + "${helper}" limited "${OUT_DIR}/mono_8bpc_limited.avif" + cp -f "${OUT_DIR}/mono_8bpc_full.avif" "${OUT_DIR}/mono_8bpc_ramp_full.avif" +} + +generate_with_avifenc() { + local workdir + workdir="$(mktemp -d)" + trap 'rm -rf "${workdir}"' RETURN + python3 - "${workdir}" <<'PY' +import pathlib, sys +out = pathlib.Path(sys.argv[1]) / "ramp.y" +data = bytearray() +for _ in range(16): + data.extend(range(256)) +out.write_bytes(data) +PY + if command -v convert >/dev/null 2>&1; then + convert -size 256x16 -depth 8 gray:"${workdir}/ramp.y" "${workdir}/ramp.png" + else + ffmpeg -y -f rawvideo -pix_fmt gray -s 256x16 -i "${workdir}/ramp.y" \ + "${workdir}/ramp.png" >/dev/null 2>&1 + fi + avifenc --yuv 400 --depth 8 --range limited --min 0 --max 0 --speed 0 \ + "${workdir}/ramp.png" "${OUT_DIR}/mono_8bpc_limited.avif" + avifenc --yuv 400 --depth 8 --range full --min 0 --max 0 --speed 0 \ + "${workdir}/ramp.png" "${OUT_DIR}/mono_8bpc_full.avif" + cp -f "${OUT_DIR}/mono_8bpc_full.avif" "${OUT_DIR}/mono_8bpc_ramp_full.avif" +} + +if generate_with_helper; then + : +elif command -v avifenc >/dev/null 2>&1; then + echo "Using avifenc fallback." >&2 + generate_with_avifenc +else + echo "Need gen_mono_ramp (libavif+aom) or avifenc." >&2 + exit 1 +fi + +echo "Wrote:" +ls -la "${OUT_DIR}/mono_8bpc_limited.avif" \ + "${OUT_DIR}/mono_8bpc_full.avif" \ + "${OUT_DIR}/mono_8bpc_ramp_full.avif" diff --git a/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifHardwareDecoderGray565Test.java b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifHardwareDecoderGray565Test.java new file mode 100644 index 0000000000..4b717f15c9 --- /dev/null +++ b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifHardwareDecoderGray565Test.java @@ -0,0 +1,210 @@ +package org.aomedia.avif.android; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assume.assumeTrue; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.ColorSpace; +import android.graphics.Paint; +import android.hardware.HardwareBuffer; +import android.os.Build; +import androidx.test.platform.app.InstrumentationRegistry; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * HardwareBuffer Gray565 packing tests for {@link AvifHardwareDecoder}. + * + *

Requires API 29+ ({@code Bitmap.wrapHardwareBuffer}). Generate mono assets with {@code + * generate_mono_test_assets.sh} before running. + */ +@RunWith(JUnit4.class) +public class AvifHardwareDecoderGray565Test { + + // Restores packed Gray565: Y ≈ 31/255·R + 252/255·G (B unused / zero). + private static final ColorMatrix GRAY565_RESTORE_MATRIX = + new ColorMatrix( + new float[] { + 31f / 255f, 252f / 255f, 0f, 0f, 0f, + 31f / 255f, 252f / 255f, 0f, 0f, 0f, + 31f / 255f, 252f / 255f, 0f, 0f, 0f, + 0f, 0f, 0f, 1f, 0f + }); + + private static ByteBuffer loadAsset(String assetPath) throws IOException { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + InputStream is = context.getAssets().open(assetPath); + ByteBuffer buffer = ByteBuffer.allocateDirect(is.available()); + Channels.newChannel(is).read(buffer); + buffer.rewind(); + return buffer; + } + + private static Bitmap decodeSoftwareArgb(ByteBuffer encoded) { + AvifDecoder.Info info = new AvifDecoder.Info(); + encoded.rewind(); + assertThat(AvifDecoder.getInfo(encoded, encoded.remaining(), info)).isTrue(); + Bitmap bitmap = Bitmap.createBitmap(info.width, info.height, Config.ARGB_8888); + encoded.rewind(); + assertThat(AvifDecoder.decode(encoded, encoded.remaining(), bitmap)).isTrue(); + return bitmap; + } + + private static Bitmap restoreGray565ToSoftware(HardwareBuffer hardwareBuffer) { + Bitmap hardwareBitmap = + Bitmap.wrapHardwareBuffer(hardwareBuffer, ColorSpace.get(ColorSpace.Named.SRGB)); + assertThat(hardwareBitmap).isNotNull(); + Bitmap software = + Bitmap.createBitmap(hardwareBitmap.getWidth(), hardwareBitmap.getHeight(), Config.ARGB_8888); + Canvas canvas = new Canvas(software); + Paint paint = new Paint(); + paint.setColorFilter(new ColorMatrixColorFilter(GRAY565_RESTORE_MATRIX)); + canvas.drawBitmap(hardwareBitmap, 0f, 0f, paint); + hardwareBitmap.recycle(); + return software; + } + + private static void assertBitmapsNearlyEqual(Bitmap actual, Bitmap expected, int tolerance) { + assertThat(actual.getWidth()).isEqualTo(expected.getWidth()); + assertThat(actual.getHeight()).isEqualTo(expected.getHeight()); + int width = actual.getWidth(); + int height = actual.getHeight(); + int[] actualPixels = new int[width * height]; + int[] expectedPixels = new int[width * height]; + actual.getPixels(actualPixels, 0, width, 0, 0, width, height); + expected.getPixels(expectedPixels, 0, width, 0, 0, width, height); + for (int i = 0; i < actualPixels.length; ++i) { + int a = actualPixels[i]; + int e = expectedPixels[i]; + assertChannelNear("R", (a >> 16) & 0xff, (e >> 16) & 0xff, tolerance, i); + assertChannelNear("G", (a >> 8) & 0xff, (e >> 8) & 0xff, tolerance, i); + assertChannelNear("B", a & 0xff, e & 0xff, tolerance, i); + assertChannelNear("A", (a >>> 24) & 0xff, (e >>> 24) & 0xff, tolerance, i); + } + } + + private static void assertChannelNear( + String channel, int actual, int expected, int tolerance, int index) { + assertWithMessage( + "%s at pixel %s: actual=%s expected=%s tol=%s", + channel, index, actual, expected, tolerance) + .that(Math.abs(actual - expected)) + .isAtMost(tolerance); + } + + @Test + public void monoAllowGray565_returnsRgb565() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/mono_8bpc_full.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ true); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGB_565); + hardwareBuffer.close(); + } + + @Test + public void monoAllowGray565False_returnsRgba8888() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/mono_8bpc_full.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ false); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGBA_8888); + hardwareBuffer.close(); + } + + @Test + public void colorImageAllowGray565_returnsRgba8888() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/fox.profile0.8bpc.yuv420.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ true); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGBA_8888); + hardwareBuffer.close(); + } + + @Test + public void alphaImageAllowGray565_returnsRgba8888() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/blue-and-magenta-crop.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ true); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGBA_8888); + hardwareBuffer.close(); + } + + @Test + public void rampFull_gray565MatchesSoftwareDecode() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/mono_8bpc_ramp_full.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ true); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGB_565); + + Bitmap restored = restoreGray565ToSoftware(hardwareBuffer); + hardwareBuffer.close(); + buffer.rewind(); + Bitmap software = decodeSoftwareArgb(buffer); + assertBitmapsNearlyEqual(restored, software, /* tolerance= */ 1); + restored.recycle(); + software.recycle(); + } + + @Test + public void limitedRangeMono_gray565MatchesSoftwareDecode() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/mono_8bpc_limited.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ true); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGB_565); + + Bitmap restored = restoreGray565ToSoftware(hardwareBuffer); + hardwareBuffer.close(); + buffer.rewind(); + Bitmap software = decodeSoftwareArgb(buffer); + assertBitmapsNearlyEqual(restored, software, /* tolerance= */ 1); + restored.recycle(); + software.recycle(); + } + + @Test + public void fullRangeMono_gray565MatchesSoftwareDecode() throws IOException { + assumeTrue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); + ByteBuffer buffer = loadAsset("avif/mono_8bpc_full.avif"); + HardwareBuffer hardwareBuffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + buffer, buffer.remaining(), 0, 0, /* threads= */ 1, /* allowGray565= */ true); + assertThat(hardwareBuffer).isNotNull(); + assertThat(hardwareBuffer.getFormat()).isEqualTo(HardwareBuffer.RGB_565); + + Bitmap restored = restoreGray565ToSoftware(hardwareBuffer); + hardwareBuffer.close(); + buffer.rewind(); + Bitmap software = decodeSoftwareArgb(buffer); + assertBitmapsNearlyEqual(restored, software, /* tolerance= */ 1); + restored.recycle(); + software.recycle(); + } +} diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java index 899418fc6a..27e4a6228f 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -56,18 +56,25 @@ public static HardwareBuffer decodeToHardwareBuffer( /** * Decodes the AVIF image into an {@link HardwareBuffer}. * - *

When {@code allowR8} is {@code true} and the image is 8-bit monochrome (YUV400) without - * alpha on a device running API 35+ that supports {@link HardwareBuffer#R_8 R_8} allocation, the - * returned buffer's {@link HardwareBuffer#getFormat()} will be {@link HardwareBuffer#R_8} (56). - * Otherwise the format is {@link HardwareBuffer#RGBA_8888} as in the overload without {@code - * allowR8}. + *

When {@code allowGray565} is {@code true} and the image is 8-bit monochrome ({@code YUV400}) + * without an alpha plane, the returned buffer's {@link HardwareBuffer#getFormat()} is {@link + * HardwareBuffer#RGB_565} (packed grayscale — see below). Otherwise the format is {@link + * HardwareBuffer#RGBA_8888} as in the overload without {@code allowGray565}. If {@code RGB_565} + * allocation fails, the decoder falls back to {@code RGBA_8888}. * - *

An {@code R_8} buffer holds a single luminance channel. Drawing it directly (for example via - * {@link android.graphics.Bitmap#wrapHardwareBuffer}) shows red-tinted intensity; callers must - * apply a {@link android.graphics.ColorMatrixColorFilter} or equivalent color transform for - * correct grayscale or RGB display. + *

Gray565 contract: {@link HardwareBuffer#RGB_565} here is not a true color + * RGB565 image. It is an 8-bit grayscale value packed into the RGB565 bit fields: * - * @param allowR8 When {@code true}, opt in to {@code R_8} output for eligible monochrome images. + *

+ * + * @param allowGray565 When {@code true}, opt in to Gray565 ({@code RGB_565}) packing for eligible + * monochrome images. * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) */ @Nullable @@ -77,9 +84,9 @@ public static HardwareBuffer decodeToHardwareBuffer( int targetWidth, int targetHeight, int threads, - boolean allowR8) { + boolean allowGray565) { return decodeToHardwareBufferNative( - encoded, length, targetWidth, targetHeight, threads, allowR8); + encoded, length, targetWidth, targetHeight, threads, allowGray565); } /** @@ -110,18 +117,18 @@ public static HardwareBuffer nextFrameHardwareBuffer( /** * Decodes the next frame of an animated AVIF into an {@link HardwareBuffer}. * - * @param allowR8 When {@code true}, opt in to {@code R_8} output for eligible monochrome frames. + * @param allowGray565 When {@code true}, opt in to Gray565 output for eligible monochrome frames. * See {@link #decodeToHardwareBuffer(ByteBuffer, int, int, int, int, boolean)} for format * selection rules and display responsibilities. */ @Nullable public static HardwareBuffer nextFrameHardwareBuffer( - AvifDecoder decoder, int targetWidth, int targetHeight, boolean allowR8) { + AvifDecoder decoder, int targetWidth, int targetHeight, boolean allowGray565) { if (decoder == null || !decoder.isAlive()) { return null; } return nextFrameHardwareBufferNative( - decoder.getNativeDecoderHandle(), targetWidth, targetHeight, allowR8); + decoder.getNativeDecoderHandle(), targetWidth, targetHeight, allowGray565); } /** Decodes the next frame at the cropped image dimensions. */ @@ -149,18 +156,18 @@ public static HardwareBuffer nthFrameHardwareBuffer( /** * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. * - * @param allowR8 When {@code true}, opt in to {@code R_8} output for eligible monochrome frames. + * @param allowGray565 When {@code true}, opt in to Gray565 output for eligible monochrome frames. * See {@link #decodeToHardwareBuffer(ByteBuffer, int, int, int, int, boolean)} for format * selection rules and display responsibilities. */ @Nullable public static HardwareBuffer nthFrameHardwareBuffer( - AvifDecoder decoder, int n, int targetWidth, int targetHeight, boolean allowR8) { + AvifDecoder decoder, int n, int targetWidth, int targetHeight, boolean allowGray565) { if (decoder == null || !decoder.isAlive()) { return null; } return nthFrameHardwareBufferNative( - decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight, allowR8); + decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight, allowGray565); } /** Decodes the nth frame at the cropped image dimensions. */ @@ -175,11 +182,11 @@ private static native HardwareBuffer decodeToHardwareBufferNative( int targetWidth, int targetHeight, int threads, - boolean allowR8); + boolean allowGray565); private static native HardwareBuffer nextFrameHardwareBufferNative( - long nativeDecoderHandle, int targetWidth, int targetHeight, boolean allowR8); + long nativeDecoderHandle, int targetWidth, int targetHeight, boolean allowGray565); private static native HardwareBuffer nthFrameHardwareBufferNative( - long nativeDecoderHandle, int n, int targetWidth, int targetHeight, boolean allowR8); + long nativeDecoderHandle, int n, int targetWidth, int targetHeight, boolean allowGray565); } diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index caa20a159f..6e07011650 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -13,17 +13,12 @@ #include #include #include -#include #include #include #include #include "avif/avif.h" -#ifndef AHARDWAREBUFFER_FORMAT_R8_UNORM -#define AHARDWAREBUFFER_FORMAT_R8_UNORM 0x38 // = 56, API 35 -#endif - #define LOG_TAG "avif_jni" #define LOGE(...) \ ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)) @@ -58,7 +53,6 @@ namespace { using AHardwareBuffer_allocate_fn = int (*)(const AHardwareBuffer_Desc*, AHardwareBuffer**); using AHardwareBuffer_describe_fn = void (*)(const AHardwareBuffer*, AHardwareBuffer_Desc*); -using AHardwareBuffer_isSupported_fn = bool (*)(const AHardwareBuffer_Desc*); using AHardwareBuffer_lock_fn = int (*)(AHardwareBuffer*, uint64_t, int32_t, const ARect*, void**); using AHardwareBuffer_unlock_fn = int (*)(AHardwareBuffer*, int32_t*); @@ -70,7 +64,6 @@ struct HardwareBufferApi { void* android_library = nullptr; AHardwareBuffer_allocate_fn allocate = nullptr; AHardwareBuffer_describe_fn describe = nullptr; - AHardwareBuffer_isSupported_fn is_supported = nullptr; AHardwareBuffer_lock_fn lock = nullptr; AHardwareBuffer_unlock_fn unlock = nullptr; AHardwareBuffer_release_fn release = nullptr; @@ -143,8 +136,6 @@ void LoadHardwareBufferApi(HardwareBufferApi* api) { load_buffer_symbol("AHardwareBuffer_allocate")); api->describe = reinterpret_cast( load_buffer_symbol("AHardwareBuffer_describe")); - api->is_supported = reinterpret_cast( - load_buffer_symbol("AHardwareBuffer_isSupported")); api->lock = reinterpret_cast( load_buffer_symbol("AHardwareBuffer_lock")); api->unlock = reinterpret_cast( @@ -395,9 +386,11 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, return res; } -avifResult AvifImageToGrayBuffer(const HardwareBufferApi& hw_api, - AHardwareBuffer* hw_buffer, - avifImage* image) { +// Packs 8-bit grayscale into RGB565 as R5[15:11]=Y[1:0], G6[10:5]=Y[7:2], +// B5[4:0]=0. Display-side ColorMatrix (31/255·R + 252/255·G) recovers Y. +avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, + AHardwareBuffer* hw_buffer, + avifImage* image) { AHardwareBuffer_Desc desc; hw_api.describe(hw_buffer, &desc); @@ -408,33 +401,37 @@ avifResult AvifImageToGrayBuffer(const HardwareBufferApi& hw_api, return AVIF_RESULT_UNKNOWN_ERROR; } - uint8_t* dst = static_cast(pixels); - const uint8_t* src_y = image->yuvPlanes[AVIF_CHAN_Y]; - const uint32_t width = image->width; - const uint32_t height = image->height; - const size_t src_row_bytes = image->yuvRowBytes[AVIF_CHAN_Y]; - - if (image->yuvRange == AVIF_RANGE_LIMITED) { - uint8_t lut[256]; - for (int v = 0; v < 256; ++v) { + uint16_t lut[256]; + for (int v = 0; v < 256; ++v) { + uint8_t y; + if (image->yuvRange == AVIF_RANGE_LIMITED) { int e = ((v - 16) * 255 + 109) / 219; if (e < 0) { e = 0; } else if (e > 255) { e = 255; } - lut[v] = static_cast(e); - } - for (uint32_t y = 0; y < height; ++y) { - const uint8_t* src_row = src_y + y * src_row_bytes; - uint8_t* dst_row = dst + y * desc.stride; - for (uint32_t x = 0; x < width; ++x) { - dst_row[x] = lut[src_row[x]]; - } + y = static_cast(e); + } else { + y = static_cast(v); } - } else { - for (uint32_t y = 0; y < height; ++y) { - std::memcpy(dst + y * desc.stride, src_y + y * src_row_bytes, width); + const uint16_t hi = static_cast(y >> 2); // upper 6 bits → G + const uint16_t lo = static_cast(y & 3); // lower 2 bits → R + lut[v] = static_cast((lo << 11) | (hi << 5)); // B=0 + } + + const uint8_t* src_y = image->yuvPlanes[AVIF_CHAN_Y]; + const uint32_t width = image->width; + const uint32_t height = image->height; + const size_t src_row_bytes = image->yuvRowBytes[AVIF_CHAN_Y]; + uint8_t* dst_bytes = static_cast(pixels); + + for (uint32_t row = 0; row < height; ++row) { + const uint8_t* src_row = src_y + row * src_row_bytes; + uint16_t* dst_row = + reinterpret_cast(dst_bytes + row * desc.stride); + for (uint32_t x = 0; x < width; ++x) { + dst_row[x] = lut[src_row[x]]; } } @@ -442,10 +439,8 @@ avifResult AvifImageToGrayBuffer(const HardwareBufferApi& hw_api, return AVIF_RESULT_OK; } -bool ShouldUseR8Output(avifImage* image, bool allow_r8, - const HardwareBufferApi& hw_api, - AHardwareBuffer_Desc* desc) { - if (!allow_r8) { +bool ShouldUseGray565Output(avifImage* image, bool allow_gray565) { + if (!allow_gray565) { return false; } if (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) { @@ -457,14 +452,6 @@ bool ShouldUseR8Output(avifImage* image, bool allow_r8, if (image->depth != 8) { return false; } - if (GetDeviceApiLevel() < 35) { - return false; - } - - desc->format = AHARDWAREBUFFER_FORMAT_R8_UNORM; - if (hw_api.is_supported != nullptr && !hw_api.is_supported(desc)) { - return false; - } return true; } @@ -510,7 +497,7 @@ avifResult AvifImageToBitmap(JNIEnv* const env, jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t dst_width, uint32_t dst_height, - bool allow_r8) { + bool allow_gray565) { const HardwareBufferApi& hw_api = GetHardwareBufferApi(); if (!hw_api.available) { LOGE("HardwareBuffer API is unavailable. Check earlier avif_jni logs for " @@ -536,18 +523,18 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY | AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; - bool use_r8 = ShouldUseR8Output(image, allow_r8, hw_api, &desc); + bool use_gray565 = ShouldUseGray565Output(image, allow_gray565); AHardwareBuffer* hw_buffer = nullptr; - if (use_r8) { - desc.format = AHARDWAREBUFFER_FORMAT_R8_UNORM; + if (use_gray565) { + desc.format = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM; if (hw_api.allocate(&desc, &hw_buffer) != 0) { - LOGE("AHardwareBuffer_allocate failed for R8; falling back to RGBA."); - use_r8 = false; + LOGE("AHardwareBuffer_allocate failed for R5G6B5; falling back to RGBA."); + use_gray565 = false; hw_buffer = nullptr; } } - if (!use_r8) { + if (!use_gray565) { desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; if (hw_api.allocate(&desc, &hw_buffer) != 0) { LOGE("AHardwareBuffer_allocate failed."); @@ -557,8 +544,8 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, hw_api.describe(hw_buffer, &desc); avifResult fill_res; - if (use_r8) { - fill_res = AvifImageToGrayBuffer(hw_api, hw_buffer, image); + if (use_gray565) { + fill_res = AvifImageToGray565Buffer(hw_api, hw_buffer, image); } else { void* pixels = nullptr; if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, @@ -589,7 +576,7 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, int target_width, int target_height, - int threads, bool allow_r8) { + int threads, bool allow_gray565) { const uint8_t* buffer = nullptr; size_t size = 0; if (!ValidateDirectBuffer(env, encoded, length, &buffer, &size)) { @@ -609,13 +596,13 @@ jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, return nullptr; } return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height, - allow_r8); + allow_gray565); } jobject NextFrameToHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, int target_width, int target_height, - bool allow_r8) { + bool allow_gray565) { const avifResult decode_result = avifDecoderNextImage(decoder->decoder); if (decode_result != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", decode_result); @@ -626,13 +613,13 @@ jobject NextFrameToHardwareBuffer(JNIEnv* const env, GetTargetDimensions(decoder, target_width, target_height, &dst_width, &dst_height); return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height, - allow_r8); + allow_gray565); } jobject NthFrameToHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t n, int target_width, int target_height, - bool allow_r8) { + bool allow_gray565) { const avifResult decode_result = avifDecoderNthImage(decoder->decoder, n); if (decode_result != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", decode_result); @@ -643,7 +630,7 @@ jobject NthFrameToHardwareBuffer(JNIEnv* const env, GetTargetDimensions(decoder, target_width, target_height, &dst_width, &dst_height); return AvifImageToJavaHardwareBuffer(env, decoder, dst_width, dst_height, - allow_r8); + allow_gray565); } avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, @@ -862,14 +849,16 @@ FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { } HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, - jint target_width, jint target_height, jint threads, jboolean allow_r8) { + jint target_width, jint target_height, jint threads, + jboolean allow_gray565) { IGNORE_UNUSED_HW_JNI_PARAMETERS; return DecodeToHardwareBuffer(env, encoded, length, target_width, - target_height, threads, allow_r8 != JNI_FALSE); + target_height, threads, + allow_gray565 != JNI_FALSE); } HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_width, - jint target_height, jboolean allow_r8) { + jint target_height, jboolean allow_gray565) { IGNORE_UNUSED_HW_JNI_PARAMETERS; AvifDecoderWrapper* const decoder = reinterpret_cast(jdecoder); @@ -877,11 +866,11 @@ HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_widt return nullptr; } return NextFrameToHardwareBuffer(env, decoder, target_width, target_height, - allow_r8 != JNI_FALSE); + allow_gray565 != JNI_FALSE); } HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, - jint target_width, jint target_height, jboolean allow_r8) { + jint target_width, jint target_height, jboolean allow_gray565) { IGNORE_UNUSED_HW_JNI_PARAMETERS; AvifDecoderWrapper* const decoder = reinterpret_cast(jdecoder); @@ -890,7 +879,7 @@ HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, } return NthFrameToHardwareBuffer(env, decoder, static_cast(n), target_width, target_height, - allow_r8 != JNI_FALSE); + allow_gray565 != JNI_FALSE); } FUNC(jstring, resultToString, jint result) { From 49743854d706676a5a948a36ef02a3cd588f6bbc Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Wed, 15 Jul 2026 10:54:31 +0900 Subject: [PATCH 12/25] Performance --- .../src/main/jni/libavif_jni.cc | 85 +++++++++++++------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 6e07011650..76bbaa76a1 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -386,14 +386,60 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, return res; } -// Packs 8-bit grayscale into RGB565 as R5[15:11]=Y[1:0], G6[10:5]=Y[7:2], -// B5[4:0]=0. Display-side ColorMatrix (31/255·R + 252/255·G) recovers Y. +// Packs 8-bit Y into RGB565 as R5[15:11]=Y[1:0], G6[10:5]=Y[7:2], B5[4:0]=0. +// Display-side ColorMatrix (31/255·R + 252/255·G) recovers Y. +constexpr uint16_t PackGray565(uint8_t y) { + const uint16_t hi = static_cast(y >> 2); // upper 6 bits → G + const uint16_t lo = static_cast(y & 3); // lower 2 bits → R + return static_cast((lo << 11) | (hi << 5)); // B=0 +} + +constexpr uint8_t LimitedToFull8(int v) { + const int e = ((v - 16) * 255 + 109) / 219; + if (e < 0) { + return 0; + } + if (e > 255) { + return 255; + } + return static_cast(e); +} + +// Full-range: identity then pack. Limited-range: expand [16,235] → [0,255] +// then pack. Compile-time tables in .rodata. +// Plain arrays (not std::array): non-const std::array::operator[] is only +// constexpr in C++17+, while the Android NDK JNI target defaults older. +struct Gray565LutTable { + uint16_t data[256]; +}; + +constexpr Gray565LutTable MakeGray565LutFull() { + Gray565LutTable table = {}; + for (int v = 0; v < 256; ++v) { + table.data[v] = PackGray565(static_cast(v)); + } + return table; +} + +constexpr Gray565LutTable MakeGray565LutLimited() { + Gray565LutTable table = {}; + for (int v = 0; v < 256; ++v) { + table.data[v] = PackGray565(LimitedToFull8(v)); + } + return table; +} + +constexpr Gray565LutTable kGray565LutFull = MakeGray565LutFull(); +constexpr Gray565LutTable kGray565LutLimited = MakeGray565LutLimited(); + +const uint16_t* Gray565Lut(avifRange range) { + return range == AVIF_RANGE_LIMITED ? kGray565LutLimited.data + : kGray565LutFull.data; +} + avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, AHardwareBuffer* hw_buffer, - avifImage* image) { - AHardwareBuffer_Desc desc; - hw_api.describe(hw_buffer, &desc); - + avifImage* image, size_t dst_row_bytes) { void* pixels = nullptr; if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, &pixels) != 0) { @@ -401,25 +447,7 @@ avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, return AVIF_RESULT_UNKNOWN_ERROR; } - uint16_t lut[256]; - for (int v = 0; v < 256; ++v) { - uint8_t y; - if (image->yuvRange == AVIF_RANGE_LIMITED) { - int e = ((v - 16) * 255 + 109) / 219; - if (e < 0) { - e = 0; - } else if (e > 255) { - e = 255; - } - y = static_cast(e); - } else { - y = static_cast(v); - } - const uint16_t hi = static_cast(y >> 2); // upper 6 bits → G - const uint16_t lo = static_cast(y & 3); // lower 2 bits → R - lut[v] = static_cast((lo << 11) | (hi << 5)); // B=0 - } - + const uint16_t* lut = Gray565Lut(image->yuvRange); const uint8_t* src_y = image->yuvPlanes[AVIF_CHAN_Y]; const uint32_t width = image->width; const uint32_t height = image->height; @@ -429,7 +457,7 @@ avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, for (uint32_t row = 0; row < height; ++row) { const uint8_t* src_row = src_y + row * src_row_bytes; uint16_t* dst_row = - reinterpret_cast(dst_bytes + row * desc.stride); + reinterpret_cast(dst_bytes + row * dst_row_bytes); for (uint32_t x = 0; x < width; ++x) { dst_row[x] = lut[src_row[x]]; } @@ -545,7 +573,10 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, avifResult fill_res; if (use_gray565) { - fill_res = AvifImageToGray565Buffer(hw_api, hw_buffer, image); + // AHardwareBuffer_Desc.stride is in pixels, not bytes (same as the RGBA + // path's `desc.stride * 4`). RGB565 is 2 bytes/pixel. + fill_res = AvifImageToGray565Buffer( + hw_api, hw_buffer, image, static_cast(desc.stride) * 2); } else { void* pixels = nullptr; if (hw_api.lock(hw_buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1, nullptr, From a7fe6772713fcd74c540ee0675b93d67747d1e82 Mon Sep 17 00:00:00 2001 From: k2w4t4h Date: Wed, 15 Jul 2026 13:24:29 +0900 Subject: [PATCH 13/25] =?UTF-8?q?dav1d=E3=81=AEVer=E3=81=82=E3=81=92?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ext/dav1d_android.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/dav1d_android.sh b/ext/dav1d_android.sh index c564321701..6bc6aaae9a 100755 --- a/ext/dav1d_android.sh +++ b/ext/dav1d_android.sh @@ -18,7 +18,7 @@ if [ $# -ne 1 ]; then echo "Usage: ${0} " exit 1 fi -git clone -b 1.5.3 --depth 1 https://code.videolan.org/videolan/dav1d.git +git clone -b 1.5.4 --depth 1 https://code.videolan.org/videolan/dav1d.git mkdir dav1d/build # This only works on linux and mac. From 3227f53ae5e63d504a401f2372a28cea04baa888 Mon Sep 17 00:00:00 2001 From: k2w4t4h Date: Wed, 15 Jul 2026 14:55:29 +0900 Subject: [PATCH 14/25] =?UTF-8?q?=E7=84=A1=E9=A7=84=E3=82=B3=E3=83=94?= =?UTF-8?q?=E3=83=BC=E3=82=92=E6=B8=9B=E3=82=89=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/jni/libavif_jni.cc | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 76bbaa76a1..844d9f5919 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -286,7 +286,7 @@ avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, std::unique_ptr& cropped_image, std::unique_ptr& - image_copy) { + scaling_image) { avifImage* image; if (decoder->decoder->image->width == decoder->crop.width && decoder->decoder->image->height == decoder->crop.height && @@ -308,19 +308,23 @@ avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, image = cropped_image.get(); } if (image->width != dst_width || image->height != dst_height) { - if (!image->imageOwnsYUVPlanes || !image->imageOwnsAlphaPlane) { - image_copy.reset(avifImageCreateEmpty()); - if (image_copy == nullptr) { - LOGE("Failed to allocate image for scaling."); + if (image == decoder->decoder->image) { + // Scale a non-owning full-image view so that the decoder image remains + // unchanged. avifImageScale() can read non-owning source planes directly + // and allocates only the destination planes. + scaling_image.reset(avifImageCreateEmpty()); + if (scaling_image == nullptr) { + LOGE("Failed to allocate image view for scaling."); *res = AVIF_RESULT_OUT_OF_MEMORY; return nullptr; } - *res = avifImageCopy(image_copy.get(), image, AVIF_PLANES_ALL); + const avifCropRect full_image = {0, 0, image->width, image->height}; + *res = avifImageSetViewRect(scaling_image.get(), image, &full_image); if (*res != AVIF_RESULT_OK) { - LOGE("Failed to make a copy of the image for scaling. Status: %d", *res); + LOGE("Failed to create image view for scaling. Status: %d", *res); return nullptr; } - image = image_copy.get(); + image = scaling_image.get(); } avifDiagnostics diag; *res = avifImageScale(image, dst_width, dst_height, &diag); @@ -359,10 +363,10 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, avifResult res; std::unique_ptr cropped_image( nullptr, avifImageDestroy); - std::unique_ptr image_copy( + std::unique_ptr scaling_image( nullptr, avifImageDestroy); avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, - cropped_image, image_copy); + cropped_image, scaling_image); if (image == nullptr) { return res; } @@ -536,10 +540,10 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, avifResult res; std::unique_ptr cropped_image( nullptr, avifImageDestroy); - std::unique_ptr image_copy( + std::unique_ptr scaling_image( nullptr, avifImageDestroy); avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, - cropped_image, image_copy); + cropped_image, scaling_image); if (image == nullptr) { return nullptr; } From b9e7361bd10a13dea3f112915661fbd1c48cefcb Mon Sep 17 00:00:00 2001 From: k2w4t4h Date: Wed, 15 Jul 2026 15:16:42 +0900 Subject: [PATCH 15/25] =?UTF-8?q?=E3=83=AA=E3=82=B5=E3=82=A4=E3=82=BA?= =?UTF-8?q?=E3=81=AE=E3=83=95=E3=82=A3=E3=83=AB=E3=82=BF=E3=82=92=E9=80=9F?= =?UTF-8?q?=E3=81=84=E3=82=84=E3=81=A4=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/scale.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scale.c b/src/scale.c index 777eca48de..30cbf9a74b 100644 --- a/src/scale.c +++ b/src/scale.c @@ -18,8 +18,9 @@ #pragma clang diagnostic pop #endif -// This should be configurable and/or smarter. kFilterBox has the highest quality but is the slowest. -#define AVIF_LIBYUV_FILTER_MODE kFilterBox +// Match the default bilinear filtering used when Compose scales images while +// avoiding the higher cost of kFilterBox. +#define AVIF_LIBYUV_FILTER_MODE kFilterBilinear avifResult avifImageScaleWithLimit(avifImage * image, uint32_t dstWidth, From efc42b375f25d8b6621a5899ed3ba4d0d14995df Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Wed, 15 Jul 2026 23:46:56 +0900 Subject: [PATCH 16/25] Neon SIMD --- .../src/main/jni/libavif_jni.cc | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 844d9f5919..df4e60c953 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -17,6 +17,10 @@ #include #include +#if defined(__ARM_NEON) || defined(__ARM_NEON__) +#include +#endif + #include "avif/avif.h" #define LOG_TAG "avif_jni" @@ -441,6 +445,71 @@ const uint16_t* Gray565Lut(avifRange range) { : kGray565LutFull.data; } +#if defined(__ARM_NEON) || defined(__ARM_NEON__) +// Pack 8 full-range Y samples: R5=Y[1:0], G6=Y[7:2], B5=0. +static uint16x8_t PackGray565Neon(uint16x8_t y) { + const uint16x8_t lo = vandq_u16(y, vdupq_n_u16(3)); + const uint16x8_t hi = vshrq_n_u16(y, 2); + return vorrq_u16(vshlq_n_u16(lo, 11), vshlq_n_u16(hi, 5)); +} + +// Limited [16,235] → full [0,255]: e = ((y-16)*255+109)/219, with y clamped. +// Uses (n * 1197) >> 18, exact for all 8-bit inputs after clamp. +static uint16x8_t LimitedToFull8Neon(uint8x8_t y8) { + y8 = vmax_u8(y8, vdup_n_u8(16)); + y8 = vmin_u8(y8, vdup_n_u8(235)); + const uint16x8_t t = vsubq_u16(vmovl_u8(y8), vdupq_n_u16(16)); + // n = t * 255 + 109 fits in uint16 (max 55984). + const uint16x8_t n = vmlaq_n_u16(vdupq_n_u16(109), t, 255); + const uint32x4_t e_lo = vshrq_n_u32(vmulq_n_u32(vmovl_u16(vget_low_u16(n)), 1197), 18); + const uint32x4_t e_hi = + vshrq_n_u32(vmulq_n_u32(vmovl_u16(vget_high_u16(n)), 1197), 18); + return vcombine_u16(vmovn_u32(e_lo), vmovn_u32(e_hi)); +} + +static void PackGray565RowNeon(const uint8_t* src, uint16_t* dst, uint32_t width, + avifRange range) { + const bool limited = range == AVIF_RANGE_LIMITED; + uint32_t x = 0; + if (limited) { + for (; x + 16 <= width; x += 16) { + const uint8x16_t y8 = vld1q_u8(src + x); + vst1q_u16(dst + x, PackGray565Neon(LimitedToFull8Neon(vget_low_u8(y8)))); + vst1q_u16(dst + x + 8, + PackGray565Neon(LimitedToFull8Neon(vget_high_u8(y8)))); + } + for (; x + 8 <= width; x += 8) { + vst1q_u16(dst + x, PackGray565Neon(LimitedToFull8Neon(vld1_u8(src + x)))); + } + } else { + for (; x + 16 <= width; x += 16) { + const uint8x16_t y8 = vld1q_u8(src + x); + vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(vget_low_u8(y8)))); + vst1q_u16(dst + x + 8, PackGray565Neon(vmovl_u8(vget_high_u8(y8)))); + } + for (; x + 8 <= width; x += 8) { + vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(vld1_u8(src + x)))); + } + } + const uint16_t* lut = Gray565Lut(range); + for (; x < width; ++x) { + dst[x] = lut[src[x]]; + } +} +#endif // __ARM_NEON + +static void PackGray565Row(const uint8_t* src, uint16_t* dst, uint32_t width, + avifRange range) { +#if defined(__ARM_NEON) || defined(__ARM_NEON__) + PackGray565RowNeon(src, dst, width, range); +#else + const uint16_t* lut = Gray565Lut(range); + for (uint32_t x = 0; x < width; ++x) { + dst[x] = lut[src[x]]; + } +#endif +} + avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, AHardwareBuffer* hw_buffer, avifImage* image, size_t dst_row_bytes) { @@ -451,20 +520,18 @@ avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, return AVIF_RESULT_UNKNOWN_ERROR; } - const uint16_t* lut = Gray565Lut(image->yuvRange); const uint8_t* src_y = image->yuvPlanes[AVIF_CHAN_Y]; const uint32_t width = image->width; const uint32_t height = image->height; const size_t src_row_bytes = image->yuvRowBytes[AVIF_CHAN_Y]; uint8_t* dst_bytes = static_cast(pixels); + const avifRange range = image->yuvRange; for (uint32_t row = 0; row < height; ++row) { const uint8_t* src_row = src_y + row * src_row_bytes; uint16_t* dst_row = reinterpret_cast(dst_bytes + row * dst_row_bytes); - for (uint32_t x = 0; x < width; ++x) { - dst_row[x] = lut[src_row[x]]; - } + PackGray565Row(src_row, dst_row, width, range); } hw_api.unlock(hw_buffer, nullptr); From 34540e098937eca048e01cf9a07e39d9f23533da Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 00:10:25 +0900 Subject: [PATCH 17/25] =?UTF-8?q?=E3=83=93=E3=83=AB=E3=83=89=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ android_jni/README.md | 13 +++++++++++ .../src/main/jni/CMakeLists.txt | 23 +++++++++++++++++++ cmake/Modules/LocalDav1d.cmake | 19 +++++++++++---- ext/dav1d_android.sh | 17 ++++++++++---- ext/libyuv_android.sh | 9 ++++++++ 6 files changed, 77 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b1bd6e99..cc8596613f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,12 @@ The changes are relative to the previous release, unless the baseline is specifi * Add the ignoreICC option to avifDecoder * avifenc: add --ignore-alpha flag to discard alpha channel on encode * avifgainmaputil: add --ignore-alpha flag to discard alpha channel +* Android JNI: enable LTO/IPO for Release native builds (dav1d/libyuv Android + scripts and LocalDav1d on Android also build with LTO) +* ext/libyuv_android.sh: disable JPEG/MJPEG (`CMAKE_DISABLE_FIND_PACKAGE_JPEG`) ### Changed since 1.4.2 +* Android JNI: use link-u/dav1d (`avif` branch) instead of videolan dav1d * Update LocalAvm.cmake: v1.0.0 * Update libyuv.cmd/LocalLibyuv.cmake: 5d03bf9ba (1949) diff --git a/android_jni/README.md b/android_jni/README.md index 2b1e0bc08c..d6ee91f1f0 100644 --- a/android_jni/README.md +++ b/android_jni/README.md @@ -39,9 +39,19 @@ $ ./dav1d_android.sh "${ANDROID_NDK_HOME}" $ cd .. ``` +`dav1d_android.sh` clones [link-u/dav1d](https://github.com/link-u/dav1d) +(`avif` branch). If `ext/dav1d` is absent, the Android JNI CMake LOCAL path +fetches the same repository/branch via `LocalDav1d.cmake`. + The Android dav1d build is configured with `-Dbitdepths=8` (8-bit AV1 only). Re-run the script after changing this option so that all ABIs are rebuilt. +The Android JNI Release build enables LTO/IPO (`AVIF_ANDROID_ENABLE_LTO`, default ON). +`dav1d_android.sh` and `libyuv_android.sh` build with LTO so those static libraries +participate in the final `libavif_android.so` link. Re-run both scripts after updating +them if you previously built without LTO. Pass `-DAVIF_ANDROID_ENABLE_LTO=OFF` via +Gradle `android.extraCMakeFlags` to disable. + Instrumented tests assume this 8-bit-only dav1d build: 10/12-bit assets are still parsed via `getInfo`, but decode APIs are expected to fail for those streams. If you rebuild dav1d with full bitdepths (`-Dbitdepths=8,16`), update the tests accordingly. @@ -66,6 +76,9 @@ $ ./libyuv_android.sh "${ANDROID_NDK_HOME}" $ cd .. ``` +`libyuv_android.sh` disables JPEG/MJPEG support (`CMAKE_DISABLE_FIND_PACKAGE_JPEG`) +since AVIF decode does not use it. + If you do not want to use libyuv, then update [CMakeLists.txt](avifandroidjni/src/main/jni/CMakeLists.txt) as follows: * Set `AVIF_LIBYUV` to `OFF`. diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt index 186831772c..0614b10716 100644 --- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt +++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt @@ -29,6 +29,29 @@ set(AVIF_LIBYUV "LOCAL" CACHE STRING "" FORCE) # To enable libgav1, change the following two variables to ON. set(AVIF_CODEC_LIBGAV1 OFF CACHE BOOL "" FORCE) +# Enable LTO for non-Debug builds so libavif, libyuv (FetchContent), and the +# JNI shared library are compiled/linked with IPO. dav1d must also be built +# with -Db_lto=true (see ext/dav1d_android.sh / LocalDav1d.cmake) for +# cross-module optimization to include the decoder. +option(AVIF_ANDROID_ENABLE_LTO "Enable LTO/IPO for the Android JNI native build" ON) +if(AVIF_ANDROID_ENABLE_LTO AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + include(CheckIPOSupported) + check_ipo_supported(RESULT avif_android_ipo_supported OUTPUT avif_android_ipo_error) + if(avif_android_ipo_supported) + # CMake may inject -fuse-ld=gold for Clang IPO; NDK r22+ removed gold. + foreach(_lang C CXX) + if(DEFINED CMAKE_${_lang}_LINK_OPTIONS_IPO) + string(REPLACE "-fuse-ld=gold" "" _avif_ipo_link "${CMAKE_${_lang}_LINK_OPTIONS_IPO}") + set(CMAKE_${_lang}_LINK_OPTIONS_IPO "${_avif_ipo_link}") + endif() + endforeach() + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) + message(STATUS "avif_android_jni: enabling LTO/IPO") + else() + message(WARNING "avif_android_jni: LTO/IPO requested but not supported: ${avif_android_ipo_error}") + endif() +endif() + # The current CMake file lives in: # $LIBAVIF_ROOT/android_jni/avifandroidjni/src/main/jni. # In order to build libavif, we need to go up 5 directories. If a different diff --git a/cmake/Modules/LocalDav1d.cmake b/cmake/Modules/LocalDav1d.cmake index 58b1549716..c22c749b9c 100644 --- a/cmake/Modules/LocalDav1d.cmake +++ b/cmake/Modules/LocalDav1d.cmake @@ -1,4 +1,7 @@ set(AVIF_DAV1D_TAG "1.5.3") +# Android JNI pulls link-u/dav1d (avif branch) instead of upstream videolan. +set(AVIF_DAV1D_ANDROID_GIT_REPOSITORY "https://github.com/link-u/dav1d.git") +set(AVIF_DAV1D_ANDROID_GIT_TAG "avif") function(avif_build_local_dav1d) set(download_step_args) @@ -8,9 +11,15 @@ function(avif_build_local_dav1d) else() message(STATUS "libavif(AVIF_CODEC_DAV1D=LOCAL): ext/dav1d not found, fetching") set(source_dir "${FETCHCONTENT_BASE_DIR}/dav1d-src") - list(APPEND download_step_args GIT_REPOSITORY https://code.videolan.org/videolan/dav1d.git GIT_TAG ${AVIF_DAV1D_TAG} - GIT_SHALLOW ON - ) + if(ANDROID) + list(APPEND download_step_args GIT_REPOSITORY ${AVIF_DAV1D_ANDROID_GIT_REPOSITORY} + GIT_TAG ${AVIF_DAV1D_ANDROID_GIT_TAG} GIT_SHALLOW ON + ) + else() + list(APPEND download_step_args GIT_REPOSITORY https://code.videolan.org/videolan/dav1d.git + GIT_TAG ${AVIF_DAV1D_TAG} GIT_SHALLOW ON + ) + endif() endif() find_program(NINJA_EXECUTABLE NAMES ninja ninja-build REQUIRED) @@ -76,6 +85,8 @@ function(avif_build_local_dav1d) if(ANDROID) set(DAV1D_BITDEPTHS_ARG -Dbitdepths=8) + # Match android_jni LTO so libdav1d.a contains bitcode for the final .so link. + set(DAV1D_LTO_ARG -Db_lto=true) endif() ExternalProject_Add( @@ -93,7 +104,7 @@ function(avif_build_local_dav1d) CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env "PATH=${PATH}" ${MESON_EXECUTABLE} setup --buildtype=release --default-library=static --prefix= --libdir=lib -Denable_asm=true -Denable_tools=false -Denable_examples=false - -Denable_tests=false ${DAV1D_BITDEPTHS_ARG} ${EXTRA_ARGS} + -Denable_tests=false ${DAV1D_BITDEPTHS_ARG} ${DAV1D_LTO_ARG} ${EXTRA_ARGS} BUILD_COMMAND ${CMAKE_COMMAND} -E env "PATH=${PATH}" ${NINJA_EXECUTABLE} -C INSTALL_COMMAND ${CMAKE_COMMAND} -E env "PATH=${PATH}" ${NINJA_EXECUTABLE} -C install BUILD_BYPRODUCTS /lib/libdav1d.a diff --git a/ext/dav1d_android.sh b/ext/dav1d_android.sh index 6bc6aaae9a..d7136f8223 100755 --- a/ext/dav1d_android.sh +++ b/ext/dav1d_android.sh @@ -9,8 +9,8 @@ # # Android NDK: https://developer.android.com/ndk/downloads # -# The git tag below is known to work, and will occasionally be updated. Feel -# free to use a more recent commit. +# Android JNI uses link-u/dav1d (avif branch) rather than upstream videolan. +# Feel free to update the branch/commit as needed. set -e @@ -18,7 +18,7 @@ if [ $# -ne 1 ]; then echo "Usage: ${0} " exit 1 fi -git clone -b 1.5.4 --depth 1 https://code.videolan.org/videolan/dav1d.git +git clone -b avif --depth 1 https://github.com/link-u/dav1d.git mkdir dav1d/build # This only works on linux and mac. @@ -33,8 +33,17 @@ ABI_LIST=("armeabi-v7a" "arm64-v8a" "x86" "x86_64") ARCH_LIST=("arm" "aarch64" "x86" "x86_64") for i in "${!ABI_LIST[@]}"; do abi="${ABI_LIST[i]}" + # -Db_lto=true so the static archive participates in the Android JNI LTO link. PATH=$PATH:${android_bin} meson setup --default-library=static --buildtype release \ --cross-file="dav1d/package/crossfiles/${ARCH_LIST[i]}-android.meson" \ - -Dbitdepths=8 -Denable_tools=false -Denable_tests=false "dav1d/build/${abi}" dav1d + -Db_lto=true -Dlogging=false -Dbitdepths=8 -Denable_tools=false -Denable_tests=false \ + -Denable_docs=false \ + -Denable_filmgrain=false \ + -Denable_422_444=false \ + -Denable_frame_delay=false \ + -Denable_superres=false \ + -Denable_warp=false \ + -Denable_compound=false \ + "dav1d/build/${abi}" dav1d PATH=$PATH:${android_bin} meson compile -C "dav1d/build/${abi}" done diff --git a/ext/libyuv_android.sh b/ext/libyuv_android.sh index d8cb38195f..ddbdffa6e9 100755 --- a/ext/libyuv_android.sh +++ b/ext/libyuv_android.sh @@ -28,10 +28,19 @@ ABI_LIST="armeabi-v7a arm64-v8a x86 x86_64" for abi in ${ABI_LIST}; do mkdir "${abi}" cd "${abi}" + # CMAKE_DISABLE_FIND_PACKAGE_JPEG: AVIF decode does not need libyuv's MJPEG + # path; keep HAVE_JPEG undefined (matches LocalLibyuv.cmake FetchContent). + # CMAKE_INTERPROCEDURAL_OPTIMIZATION so libyuv.a contains LTO bitcode for the + # Android JNI shared library link. Strip -fuse-ld=gold which NDK r22+ removed. cmake ../.. \ -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ -DCMAKE_TOOLCHAIN_FILE=${1}/build/cmake/android.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE \ + -DUNIT_TEST=OFF \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_C_LINK_OPTIONS_IPO= \ + -DCMAKE_CXX_LINK_OPTIONS_IPO= \ -DANDROID_ABI=${abi} make yuv cd .. From 4ce90c6e71905d7a58a2a0a0cb05703266f8e1f0 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 16:05:30 +0900 Subject: [PATCH 18/25] =?UTF-8?q?=E8=89=B2=E3=80=85=E5=89=8A=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 18 +- .../aomedia/avif/android/AvifDecoderTest.java | 5 +- .../src/main/jni/CMakeLists.txt | 10 + .../src/main/jni/libavif_jni.cc | 11 +- ext/check_dav1d_syms.sh | 15 + ext/libyuv_android.sh | 3 +- ext/rebuild_dav1d_arm64.sh | 13 + src/encode_stubs.c | 134 ++++++++ src/gainmap.c | 106 ++++++ src/read.c | 6 + src/reformat.c | 51 +-- src/reformat_libyuv.c | 308 ++++++++---------- 12 files changed, 463 insertions(+), 217 deletions(-) create mode 100644 ext/check_dav1d_syms.sh create mode 100644 ext/rebuild_dav1d_arm64.sh create mode 100644 src/encode_stubs.c diff --git a/CMakeLists.txt b/CMakeLists.txt index c9a7f82df5..76d06f4199 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -76,6 +76,10 @@ option(AVIF_ENABLE_WERROR "Treat all compiler warnings as errors" OFF) option(AVIF_ENABLE_EXPERIMENTAL_MINI "Enable experimental reduced header" OFF) option(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI "Enable experimental PixelInformationProperty syntax from HEIF 3rd Ed. Amd2" OFF) +# Decode-only: exclude encoder (src/write.c), colrconvert.c, sampletransform.c; +# stub encoder / gainmap Apply-Compute / RGB→YUV / sample-transform APIs; slim reformat paths. +option(AVIF_DECODE_ONLY "Build decode-only (no encoder / write.c)" OFF) + set(AVIF_PKG_CONFIG_EXTRA_LIBS_PRIVATE "") set(AVIF_PKG_CONFIG_EXTRA_REQUIRES_PRIVATE "") @@ -358,11 +362,14 @@ if(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) add_compile_definitions(AVIF_ENABLE_EXPERIMENTAL_EXTENDED_PIXI) endif() +if(AVIF_DECODE_ONLY) + add_compile_definitions(AVIF_DECODE_ONLY) +endif() + set(AVIF_SRCS src/alpha.c src/avif.c src/colr.c - src/colrconvert.c src/diag.c src/exif.c src/gainmap.c @@ -375,13 +382,18 @@ set(AVIF_SRCS src/reformat.c src/reformat_libsharpyuv.c src/reformat_libyuv.c - src/sampletransform.c src/scale.c src/stream.c src/utils.c - src/write.c ) +if(AVIF_DECODE_ONLY) + list(APPEND AVIF_SRCS src/encode_stubs.c) + message(STATUS "libavif: AVIF_DECODE_ONLY enabled (encoder/write.c, colrconvert.c, sampletransform.c excluded)") +else() + list(APPEND AVIF_SRCS src/write.c src/colrconvert.c src/sampletransform.c) +endif() + if(AVIF_ENABLE_COMPLIANCE_WARDEN) if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/ext/ComplianceWarden") message(FATAL_ERROR "AVIF_ENABLE_COMPLIANCE_WARDEN: ext/ComplianceWarden is missing, bailing out") 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 f00033ab8f..e8f3060dd1 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 @@ -140,9 +140,8 @@ public static List data() throws IOException { for (Image image : IMAGES) { // Test ARGB_8888 for all files. list.add(new Object[] {Config.ARGB_8888, image}); - // For 8bpc files and animated files, test only RGB_565 (F16 is flaky for animated files on - // x86 emulators). For other files, test only RGBA_F16. - Config testConfig = (image.depth == 8 || image.isAnimated) ? Config.RGB_565 : Config.RGBA_F16; + // Test ARGB_8888 and RGB_565 for 8-bit display targets (RGBA_F16 unsupported). + Config testConfig = Config.RGB_565; list.add(new Object[] {testConfig, image}); } return list; diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt index 0614b10716..ce022a9327 100644 --- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt +++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt @@ -29,6 +29,16 @@ set(AVIF_LIBYUV "LOCAL" CACHE STRING "" FORCE) # To enable libgav1, change the following two variables to ON. set(AVIF_CODEC_LIBGAV1 OFF CACHE BOOL "" FORCE) +# Android JNI is decode-only: drop write.c / encoder implementation. +set(AVIF_DECODE_ONLY ON CACHE BOOL "" FORCE) +set(AVIF_CODEC_AOM OFF CACHE STRING "" FORCE) +set(AVIF_CODEC_RAV1E OFF CACHE STRING "" FORCE) +set(AVIF_CODEC_SVT OFF CACHE STRING "" FORCE) +set(AVIF_CODEC_AVM OFF CACHE STRING "" FORCE) +set(AVIF_LIBSHARPYUV OFF CACHE STRING "" FORCE) +set(AVIF_BUILD_APPS OFF CACHE BOOL "" FORCE) +set(AVIF_BUILD_TESTS OFF CACHE BOOL "" FORCE) + # Enable LTO for non-Debug builds so libavif, libyuv (FetchContent), and the # JNI shared library are compiled/linked with IPO. dav1d must also be built # with -Db_lto=true (see ext/dav1d_android.sh / LocalDav1d.cmake) for diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index df4e60c953..fc4dddb67e 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -231,6 +231,7 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, decoder->decoder->maxThreads = threads; decoder->decoder->ignoreXMP = AVIF_TRUE; decoder->decoder->ignoreExif = AVIF_TRUE; + decoder->decoder->ignoreICC = AVIF_TRUE; // Turn off libavif's 'clap' (clean aperture) property validation. This allows // us to detect and ignore streams that have an invalid 'clap' property @@ -562,10 +563,9 @@ avifResult AvifImageToBitmap(JNIEnv* const env, LOGE("AndroidBitmap_getInfo failed."); return AVIF_RESULT_UNKNOWN_ERROR; } - // Ensure that the bitmap format is RGBA_8888, RGB_565 or RGBA_F16. + // Ensure that the bitmap format is RGBA_8888 or RGB_565 (8-bit display only). if (bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 && - bitmap_info.format != ANDROID_BITMAP_FORMAT_RGB_565 && - bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_F16) { + bitmap_info.format != ANDROID_BITMAP_FORMAT_RGB_565) { LOGE("Bitmap format (%d) is not supported.", bitmap_info.format); return AVIF_RESULT_NOT_IMPLEMENTED; } @@ -579,10 +579,7 @@ avifResult AvifImageToBitmap(JNIEnv* const env, avifRGBFormat format = AVIF_RGB_FORMAT_RGBA; int depth = 8; avifBool is_float = AVIF_FALSE; - if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGBA_F16) { - depth = 16; - is_float = AVIF_TRUE; - } else if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGB_565) { + if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGB_565) { format = AVIF_RGB_FORMAT_RGB_565; } diff --git a/ext/check_dav1d_syms.sh b/ext/check_dav1d_syms.sh new file mode 100644 index 0000000000..c026498d11 --- /dev/null +++ b/ext/check_dav1d_syms.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -e +cd /mnt/c/linux/git/libavif/ext/dav1d/build/arm64-v8a +ls -la src/libdav1d.a +echo "==== config ====" +grep -E 'CONFIG_WARP|CONFIG_COMPOUND' config.h +echo "==== symbols ====" +# Prefer llvm-nm from NDK if available +NM=nm +if command -v llvm-nm >/dev/null 2>&1; then NM=llvm-nm; fi +NDK_NM=/mnt/c/linux/android-sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm +if [ -x "$NDK_NM" ]; then NM="$NDK_NM"; fi +"$NM" src/libdav1d.a 2>/dev/null | grep -E 'obmc_masks|mc_warp_filter|blend_h_8bpc|warp_affine_8x8_8bpc' || echo "(no matching symbols — good if empty for U)" +echo "==== undefined count ====" +"$NM" src/libdav1d.a 2>/dev/null | grep -E ' U dav1d_obmc_masks| U dav1d_mc_warp_filter' || echo "no undefined obmc/warp refs" diff --git a/ext/libyuv_android.sh b/ext/libyuv_android.sh index ddbdffa6e9..b7ba3947e2 100755 --- a/ext/libyuv_android.sh +++ b/ext/libyuv_android.sh @@ -13,13 +13,12 @@ if [ $# -ne 1 ]; then exit 1 fi -git clone --single-branch https://chromium.googlesource.com/libyuv/libyuv +git clone -b avif --depth 1 https://github.com/link-u/libyuv.git cd libyuv : # When changing the commit below to a newer version of libyuv, it is best to make sure it is being used by chromium, : # because the test suite of chromium provides additional test coverage of libyuv. : # It can be looked up at https://source.chromium.org/chromium/chromium/src/+/main:DEPS?q=libyuv. -git checkout 5d03bf9ba mkdir build cd build diff --git a/ext/rebuild_dav1d_arm64.sh b/ext/rebuild_dav1d_arm64.sh new file mode 100644 index 0000000000..cb14e76102 --- /dev/null +++ b/ext/rebuild_dav1d_arm64.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -ex +NDK=/mnt/c/linux/android-sdk/ndk/25.2.9519653 +export PATH="$PATH:$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" +which meson ninja +cd /mnt/c/linux/git/libavif/ext/dav1d/build/arm64-v8a +ninja -t targets 2>&1 | grep -i 'mc\|dav1d' | head -40 +echo "==== explain ====" +ninja -d explain src/libdav1d.a 2>&1 | tail -50 +echo "==== rebuild ====" +ninja -C /mnt/c/linux/git/libavif/ext/dav1d/build/arm64-v8a -t clean +ninja -C /mnt/c/linux/git/libavif/ext/dav1d/build/arm64-v8a src/libdav1d.a +ls -la src/libdav1d.a diff --git a/src/encode_stubs.c b/src/encode_stubs.c new file mode 100644 index 0000000000..a24f7b3077 --- /dev/null +++ b/src/encode_stubs.c @@ -0,0 +1,134 @@ +// Copyright 2026. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +// Decode-only build stubs for APIs excluded from the Android slim build +// (src/write.c and src/sampletransform.c are not compiled). + +#include "avif/internal.h" + +avifEncoder * avifEncoderCreate(void) +{ + return NULL; +} + +void avifEncoderDestroy(avifEncoder * encoder) +{ + (void)encoder; +} + +avifResult avifEncoderWrite(avifEncoder * encoder, const avifImage * image, avifRWData * output) +{ + (void)encoder; + (void)image; + (void)output; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, avifAddImageFlags addImageFlags) +{ + (void)encoder; + (void)image; + (void)durationInTimescales; + (void)addImageFlags; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifEncoderAddImageGrid(avifEncoder * encoder, + uint32_t gridCols, + uint32_t gridRows, + const avifImage * const * cellImages, + avifAddImageFlags addImageFlags) +{ + (void)encoder; + (void)gridCols; + (void)gridRows; + (void)cellImages; + (void)addImageFlags; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output) +{ + (void)encoder; + (void)output; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifEncoderSetCodecSpecificOption(avifEncoder * encoder, const char * key, const char * value) +{ + (void)encoder; + (void)key; + (void)value; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +size_t avifEncoderGetGainMapSizeBytes(avifEncoder * encoder) +{ + (void)encoder; + return 0; +} + +//------------------------------------------------------------------------------ +// Sample Transform stubs (src/sampletransform.c excluded) + +avifBool avifSampleTransformExpressionIsValid(const avifSampleTransformExpression * tokens, uint32_t numInputImageItems) +{ + (void)tokens; + (void)numInputImageItems; + return AVIF_FALSE; +} + +avifBool avifSampleTransformExpressionIsEquivalentTo(const avifSampleTransformExpression * a, const avifSampleTransformExpression * b) +{ + (void)a; + (void)b; + return AVIF_FALSE; +} + +avifResult avifSampleTransformRecipeToExpression(avifSampleTransformRecipe recipe, avifSampleTransformExpression * expression) +{ + (void)recipe; + (void)expression; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifSampleTransformExpressionToRecipe(const avifSampleTransformExpression * expression, avifSampleTransformRecipe * recipe) +{ + (void)expression; + (void)recipe; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifImageApplyExpression(avifImage * dstImage, + avifSampleTransformBitDepth bitDepth, + const avifSampleTransformExpression * expression, + uint8_t numInputImageItems, + const avifImage * inputImageItems[], + avifPlanesFlags planes) +{ + (void)dstImage; + (void)bitDepth; + (void)expression; + (void)numInputImageItems; + (void)inputImageItems; + (void)planes; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifImageApplyOperations(avifImage * dstImage, + avifSampleTransformBitDepth bitDepth, + uint32_t numTokens, + const avifSampleTransformToken tokens[], + uint8_t numInputImageItems, + const avifImage * inputImageItems[], + avifPlanesFlags planes) +{ + (void)dstImage; + (void)bitDepth; + (void)numTokens; + (void)tokens; + (void)numInputImageItems; + (void)inputImageItems; + (void)planes; + return AVIF_RESULT_NOT_IMPLEMENTED; +} diff --git a/src/gainmap.c b/src/gainmap.c index d87f96a8ba..337962b698 100644 --- a/src/gainmap.c +++ b/src/gainmap.c @@ -7,6 +7,106 @@ #include #include +#if defined(AVIF_DECODE_ONLY) + +// Decode-only: keep metadata parse/validate helpers; stub Apply/Compute (and colrconvert callers). + +avifResult avifRGBImageApplyGainMap(const avifRGBImage * baseImage, + avifColorPrimaries baseColorPrimaries, + avifTransferCharacteristics baseTransferCharacteristics, + const avifGainMap * gainMap, + float hdrHeadroom, + avifColorPrimaries outputColorPrimaries, + avifTransferCharacteristics outputTransferCharacteristics, + avifRGBImage * toneMappedImage, + avifContentLightLevelInformationBox * clli, + avifDiagnostics * diag) +{ + (void)baseImage; + (void)baseColorPrimaries; + (void)baseTransferCharacteristics; + (void)gainMap; + (void)hdrHeadroom; + (void)outputColorPrimaries; + (void)outputTransferCharacteristics; + (void)toneMappedImage; + (void)clli; + if (diag) { + avifDiagnosticsClearError(diag); + avifDiagnosticsPrintf(diag, "Gain map apply is disabled in decode-only builds"); + } + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifImageApplyGainMap(const avifImage * baseImage, + const avifGainMap * gainMap, + float hdrHeadroom, + avifColorPrimaries outputColorPrimaries, + avifTransferCharacteristics outputTransferCharacteristics, + avifRGBImage * toneMappedImage, + avifContentLightLevelInformationBox * clli, + avifDiagnostics * diag) +{ + (void)baseImage; + (void)gainMap; + (void)hdrHeadroom; + (void)outputColorPrimaries; + (void)outputTransferCharacteristics; + (void)toneMappedImage; + (void)clli; + if (diag) { + avifDiagnosticsClearError(diag); + avifDiagnosticsPrintf(diag, "Gain map apply is disabled in decode-only builds"); + } + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, size_t numPixels, float * rangeMin, float * rangeMax) +{ + (void)gainMapF; + (void)numPixels; + (void)rangeMin; + (void)rangeMax; + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifRGBImageComputeGainMap(const avifRGBImage * baseRgbImage, + avifColorPrimaries baseColorPrimaries, + avifTransferCharacteristics baseTransferCharacteristics, + const avifRGBImage * altRgbImage, + avifColorPrimaries altColorPrimaries, + avifTransferCharacteristics altTransferCharacteristics, + avifGainMap * gainMap, + avifDiagnostics * diag) +{ + (void)baseRgbImage; + (void)baseColorPrimaries; + (void)baseTransferCharacteristics; + (void)altRgbImage; + (void)altColorPrimaries; + (void)altTransferCharacteristics; + (void)gainMap; + if (diag) { + avifDiagnosticsClearError(diag); + avifDiagnosticsPrintf(diag, "Gain map compute is disabled in decode-only builds"); + } + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage * altImage, avifGainMap * gainMap, avifDiagnostics * diag) +{ + (void)baseImage; + (void)altImage; + (void)gainMap; + if (diag) { + avifDiagnosticsClearError(diag); + avifDiagnosticsPrintf(diag, "Gain map compute is disabled in decode-only builds"); + } + return AVIF_RESULT_NOT_IMPLEMENTED; +} + +#else // !AVIF_DECODE_ONLY + // NaN-safe clamp to [0, 1]. AVIF_CLAMP passes NaN through because IEEE 754 // comparisons with NaN always return false. fmaxf/fminf return the non-NaN // argument per C99 §7.12.12, so this clamps NaN to 0. @@ -428,6 +528,8 @@ avifResult avifFindMinMaxWithoutOutliers(const float * gainMapF, size_t numPixel return AVIF_RESULT_OK; } +#endif // !AVIF_DECODE_ONLY + avifResult avifGainMapValidateMetadata(const avifGainMap * gainMap, avifDiagnostics * diag) { for (int i = 0; i < 3; ++i) { @@ -486,6 +588,8 @@ avifBool avifSameGainMapAltMetadata(const avifGainMap * a, const avifGainMap * b return AVIF_TRUE; } +#if !defined(AVIF_DECODE_ONLY) + static const float kEpsilon = 1e-10f; // Decides which of 'basePrimaries' or 'altPrimaries' should be used for doing gain map math when creating a gain map. @@ -911,3 +1015,5 @@ avifResult avifImageComputeGainMap(const avifImage * baseImage, const avifImage avifRGBImageFreePixels(&altImageRgb); return res; } + +#endif // !AVIF_DECODE_ONLY diff --git a/src/read.c b/src/read.c index fb8226f5f0..ae94a9cc00 100644 --- a/src/read.c +++ b/src/read.c @@ -6001,6 +6001,11 @@ static avifResult avifDecoderCheckGainMapProperties(avifDecoder * decoder, const // and in the same 'altr' group as the primary image item. Returns NULL otherwise. static avifDecoderItem * avifDecoderDataFindSampleTransformImageItem(avifDecoderData * data) { +#if defined(AVIF_DECODE_ONLY) + // Sample Transform / bit-depth extension disabled for Android 8-bit decode-only builds. + (void)data; + return NULL; +#else for (uint32_t itemIndex = 0; itemIndex < data->meta->items.count; ++itemIndex) { avifDecoderItem * item = data->meta->items.item[itemIndex]; if (!memcmp(item->type, "sato", 4) && item->id != data->meta->primaryItemID && item->size != 0 && @@ -6010,6 +6015,7 @@ static avifDecoderItem * avifDecoderDataFindSampleTransformImageItem(avifDecoder } } return NULL; +#endif } static avifResult avifDecoderGenerateImageTiles(avifDecoder * decoder, avifTileInfo * info, avifDecoderItem * item, avifItemCategory itemCategory) diff --git a/src/reformat.c b/src/reformat.c index d1a511b87b..6832757842 100644 --- a/src/reformat.c +++ b/src/reformat.c @@ -220,6 +220,11 @@ static int avifYUVColorSpaceInfoUVToUNorm(avifYUVColorSpaceInfo * info, float v) avifResult avifImageRGBToYUV(avifImage * image, const avifRGBImage * rgb) { +#if defined(AVIF_DECODE_ONLY) + (void)image; + (void)rgb; + return AVIF_RESULT_NOT_IMPLEMENTED; +#else if (!rgb->pixels || rgb->format == AVIF_RGB_FORMAT_RGB_565) { return AVIF_RESULT_REFORMAT_FAILED; } @@ -568,6 +573,7 @@ avifResult avifImageRGBToYUV(avifImage * image, const avifRGBImage * rgb) } } return AVIF_RESULT_OK; +#endif // !AVIF_DECODE_ONLY } // Allocates and fills look-up tables for going from YUV limited/full unorm -> full range RGB FP32. @@ -977,6 +983,7 @@ static avifResult avifImageYUVAnyToRGBAnySlow(const avifImage * image, return AVIF_RESULT_OK; } +#if !defined(AVIF_DECODE_ONLY) static avifResult avifImageYUV16ToRGB16Color(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) { const float kr = state->yuv.kr; @@ -1307,6 +1314,7 @@ static avifResult avifImageIdentity8ToRGB8ColorFullRange(const avifImage * image } return AVIF_RESULT_OK; } +#endif // !AVIF_DECODE_ONLY static avifResult avifImageYUV8ToRGB8Color(const avifImage * image, avifRGBImage * rgb, avifReformatState * state) { @@ -1418,28 +1426,9 @@ typedef union avifF16 static avifResult avifRGBImageToF16(avifRGBImage * rgb) { - avifResult libyuvResult = AVIF_RESULT_NOT_IMPLEMENTED; - if (!rgb->avoidLibYUV) { - libyuvResult = avifRGBImageToF16LibYUV(rgb); - } - if (libyuvResult != AVIF_RESULT_NOT_IMPLEMENTED) { - return libyuvResult; - } - const size_t channelCount = avifRGBFormatChannelCount(rgb->format); - const float scale = 1.0f / ((1 << rgb->depth) - 1); - const float multiplier = F16_MULTIPLIER * scale; - uint16_t * pixelRowBase = (uint16_t *)rgb->pixels; - const uint32_t stride = rgb->rowBytes >> 1; - for (size_t j = 0; j < rgb->height; ++j) { - uint16_t * pixel = pixelRowBase; - for (size_t i = 0; i < rgb->width * channelCount; ++i, ++pixel) { - avifF16 f16; - f16.f = *pixel * multiplier; - *pixel = (uint16_t)(f16.u32 >> 13); - } - pixelRowBase += stride; - } - return AVIF_RESULT_OK; + // Android slim: 8-bit display only; F16 / HalfFloatPlane path removed. + (void)rgb; + return AVIF_RESULT_NOT_IMPLEMENTED; } static avifResult avifImageYUVToRGBImpl(const avifImage * image, avifRGBImage * rgb, avifReformatState * state, avifAlphaMultiplyMode alphaMultiplyMode) @@ -1507,13 +1496,23 @@ static avifResult avifImageYUVToRGBImpl(const avifImage * image, avifRGBImage * // if we can't do alpha (un)multiply as a separated post step (destination format doesn't have alpha). if (state->yuv.mode == AVIF_REFORMAT_MODE_IDENTITY) { +#if !defined(AVIF_DECODE_ONLY) if ((image->depth == 8) && (rgb->depth == 8) && (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) && (image->yuvRange == AVIF_RANGE_FULL)) { convertResult = avifImageIdentity8ToRGB8ColorFullRange(image, rgb, state); } // TODO: Add more fast paths for identity +#endif } else if (state->yuv.mode == AVIF_REFORMAT_MODE_YUV_COEFFICIENTS) { +#if defined(AVIF_DECODE_ONLY) + // 8-bit YUV → 8-bit RGB only (420/400 enforced by avifImageYUVToRGB). + if (hasColor) { + convertResult = avifImageYUV8ToRGB8Color(image, rgb, state); + } else { + convertResult = avifImageYUV8ToRGB8Mono(image, rgb, state); + } +#else if (image->depth > 8) { // yuv:u16 @@ -1555,6 +1554,7 @@ static avifResult avifImageYUVToRGBImpl(const avifImage * image, avifRGBImage * } } } +#endif } } @@ -1648,6 +1648,13 @@ static avifBool avifJoinYUVToRGBThread(YUVToRGBThreadData * tdata) avifResult avifImageYUVToRGB(const avifImage * image, avifRGBImage * rgb) { +#if defined(AVIF_DECODE_ONLY) + // Android slim: 8-bit YUV420/YUV400 → 8-bit RGB only (no F16 / 422 / 444 / high bit depth). + if (image->depth != 8 || rgb->depth != 8 || rgb->isFloat || + (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV420 && image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)) { + return AVIF_RESULT_NOT_IMPLEMENTED; + } +#endif // It is okay for rgb->maxThreads to be equal to zero in order to allow clients to zero initialize the avifRGBImage struct // with memset. if (!image->yuvPlanes[AVIF_CHAN_Y] || rgb->maxThreads < 0) { diff --git a/src/reformat_libyuv.c b/src/reformat_libyuv.c index 18329c80b6..317345185f 100644 --- a/src/reformat_libyuv.c +++ b/src/reformat_libyuv.c @@ -137,6 +137,12 @@ unsigned int avifLibYUVVersion(void) #define I400ToARGBMatrix NULL #endif +//-------------------------------------------------------------------------------------------------- +// RGB to YUV (encode path) +// +// Android JNI decode-only build: RGB→YUV tables/helpers are unused. Keep stubs so the public +// API still links, but drop the conversion LUTs and libyuv encode entry points for size. +#if 0 // Two-step replacement for the conversions to 8-bit BT.601 YUV which are missing from libyuv. static int avifReorderARGBThenConvertToYUV(int (*ReorderARGB)(const uint8_t *, int, uint8_t *, int, int, int), int (*ConvertToYUV)(const uint8_t *, int, uint8_t *, int, uint8_t *, int, uint8_t *, int, int, int), @@ -379,6 +385,14 @@ avifResult avifImageRGBToYUVLibYUV8bpc(avifImage * image, const avifRGBImage * r // TODO: Use SplitRGBPlane() for AVIF_MATRIX_COEFFICIENTS_IDENTITY if faster than the built-in implementation return AVIF_RESULT_NOT_IMPLEMENTED; } +#else +avifResult avifImageRGBToYUVLibYUV(avifImage * image, const avifRGBImage * rgb) +{ + (void)image; + (void)rgb; + return AVIF_RESULT_NOT_IMPLEMENTED; +} +#endif // RGB to YUV (encode path disabled) //-------------------------------------------------------------------------------------------------- // YUV to RGB @@ -465,6 +479,7 @@ typedef int (*YUVAToRGBMatrix)(const uint8_t *, int, int, int); +#if 0 // Android slim: high-bitdepth conversion typedefs unused. typedef int (*YUVToRGBMatrixFilterHighBitDepth)(const uint16_t *, int, const uint16_t *, @@ -517,6 +532,7 @@ typedef int (*YUVAToRGBMatrixHighBitDepth)(const uint16_t *, int, int, int); +#endif // At most one pointer in this struct will be not-NULL. typedef struct @@ -526,10 +542,12 @@ typedef struct YUVAToRGBMatrixFilter yuvaToRgbMatrixFilter; YUVToRGBMatrix yuvToRgbMatrix; YUVAToRGBMatrix yuvaToRgbMatrix; +#if 0 // Android slim: high-bitdepth function pointers unused. YUVToRGBMatrixFilterHighBitDepth yuvToRgbMatrixFilterHighBitDepth; YUVAToRGBMatrixFilterHighBitDepth yuvaToRgbMatrixFilterHighBitDepth; YUVToRGBMatrixHighBitDepth yuvToRgbMatrixHighBitDepth; YUVAToRGBMatrixHighBitDepth yuvaToRgbMatrixHighBitDepth; +#endif } LibyuvConversionFunction; // Only allow nearest-neighbor filter if explicitly specified or left as default. @@ -548,65 +566,68 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, LibyuvConversionFunction * lcf) { // Lookup table for 8-bit YUV400 to 8-bit RGB Matrix. + // Android slim: RGBA (+ YUV400) only; other RGB layouts unused by JNI. static const YUV400ToRGBMatrix lutYuv400ToRgbMatrix[AVIF_RGB_FORMAT_COUNT] = { // // AVIF_RGB_FORMAT_ NULL, // RGB I400ToARGBMatrix, // RGBA NULL, // ARGB NULL, // BGR - I400ToARGBMatrix, // BGRA + NULL, // BGRA NULL, // ABGR NULL, // RGB_565 }; // Lookup table for 8-bit YUV To 8-bit RGB Matrix (with filter). + // Android slim: YUV420 + RGBA only. static const YUVToRGBMatrixFilter lutYuvToRgbMatrixFilter[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { - // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ - { NULL, NULL, I422ToRGB24MatrixFilter, I420ToRGB24MatrixFilter, NULL }, // RGB - { NULL, NULL, I422ToARGBMatrixFilter, I420ToARGBMatrixFilter, NULL }, // RGBA - { NULL, NULL, NULL, NULL, NULL }, // ARGB - { NULL, NULL, I422ToRGB24MatrixFilter, I420ToRGB24MatrixFilter, NULL }, // BGR - { NULL, NULL, I422ToARGBMatrixFilter, I420ToARGBMatrixFilter, NULL }, // BGRA - { NULL, NULL, NULL, NULL, NULL }, // ABGR - { NULL, NULL, NULL, NULL, NULL }, // RGB_565 + // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ + { NULL, NULL, NULL, NULL, NULL }, // RGB + { NULL, NULL, NULL, I420ToARGBMatrixFilter, NULL }, // RGBA + { NULL, NULL, NULL, NULL, NULL }, // ARGB + { NULL, NULL, NULL, NULL, NULL }, // BGR + { NULL, NULL, NULL, NULL, NULL }, // BGRA + { NULL, NULL, NULL, NULL, NULL }, // ABGR + { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }; // Lookup table for 8-bit YUVA To 8-bit RGB Matrix (with filter). static const YUVAToRGBMatrixFilter lutYuvaToRgbMatrixFilter[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { - // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ - { NULL, NULL, NULL, NULL, NULL }, // RGB - { NULL, NULL, I422AlphaToARGBMatrixFilter, I420AlphaToARGBMatrixFilter, NULL }, // RGBA - { NULL, NULL, NULL, NULL, NULL }, // ARGB - { NULL, NULL, NULL, NULL, NULL }, // BGR - { NULL, NULL, I422AlphaToARGBMatrixFilter, I420AlphaToARGBMatrixFilter, NULL }, // BGRA - { NULL, NULL, NULL, NULL, NULL }, // ABGR - { NULL, NULL, NULL, NULL, NULL }, // RGB_565 + // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ + { NULL, NULL, NULL, NULL, NULL }, // RGB + { NULL, NULL, NULL, I420AlphaToARGBMatrixFilter, NULL }, // RGBA + { NULL, NULL, NULL, NULL, NULL }, // ARGB + { NULL, NULL, NULL, NULL, NULL }, // BGR + { NULL, NULL, NULL, NULL, NULL }, // BGRA + { NULL, NULL, NULL, NULL, NULL }, // ABGR + { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }; // Lookup table for 8-bit YUV To 8-bit RGB Matrix (4:4:4 or nearest-neighbor filter). static const YUVToRGBMatrix lutYuvToRgbMatrix[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { - // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ - { NULL, I444ToRGB24Matrix, NULL, I420ToRGB24Matrix, NULL }, // RGB - { NULL, I444ToARGBMatrix, I422ToARGBMatrix, I420ToARGBMatrix, NULL }, // RGBA - { NULL, NULL, I422ToRGBAMatrix, I420ToRGBAMatrix, NULL }, // ARGB - { NULL, I444ToRGB24Matrix, NULL, I420ToRGB24Matrix, NULL }, // BGR - { NULL, I444ToARGBMatrix, I422ToARGBMatrix, I420ToARGBMatrix, NULL }, // BGRA - { NULL, NULL, I422ToRGBAMatrix, I420ToRGBAMatrix, NULL }, // ABGR - { NULL, NULL, I422ToRGB565Matrix, I420ToRGB565Matrix, NULL }, // RGB_565 + // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ + { NULL, NULL, NULL, NULL, NULL }, // RGB + { NULL, NULL, NULL, I420ToARGBMatrix, NULL }, // RGBA + { NULL, NULL, NULL, NULL, NULL }, // ARGB + { NULL, NULL, NULL, NULL, NULL }, // BGR + { NULL, NULL, NULL, NULL, NULL }, // BGRA + { NULL, NULL, NULL, NULL, NULL }, // ABGR + { NULL, NULL, NULL, I420ToRGB565Matrix, NULL }, // RGB_565 }; // Lookup table for 8-bit YUVA To 8-bit RGB Matrix (4:4:4 or nearest-neighbor filter). static const YUVAToRGBMatrix lutYuvaToRgbMatrix[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { - // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ - { NULL, NULL, NULL, NULL, NULL }, // RGB - { NULL, I444AlphaToARGBMatrix, I422AlphaToARGBMatrix, I420AlphaToARGBMatrix, NULL }, // RGBA - { NULL, NULL, NULL, NULL, NULL }, // ARGB - { NULL, NULL, NULL, NULL, NULL }, // BGR - { NULL, I444AlphaToARGBMatrix, I422AlphaToARGBMatrix, I420AlphaToARGBMatrix, NULL }, // BGRA - { NULL, NULL, NULL, NULL, NULL }, // ABGR - { NULL, NULL, NULL, NULL, NULL }, // RGB_565 + // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ + { NULL, NULL, NULL, NULL, NULL }, // RGB + { NULL, NULL, NULL, I420AlphaToARGBMatrix, NULL }, // RGBA + { NULL, NULL, NULL, NULL, NULL }, // ARGB + { NULL, NULL, NULL, NULL, NULL }, // BGR + { NULL, NULL, NULL, NULL, NULL }, // BGRA + { NULL, NULL, NULL, NULL, NULL }, // ABGR + { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }; +#if 0 // Android slim: 10/12-bit libyuv YUV→RGB paths unused (8-bit decode only). // Lookup table for YUV To RGB Matrix (with filter). First dimension is for the YUV bit depth. static const YUVToRGBMatrixFilterHighBitDepth lutYuvToRgbMatrixFilterHighBitDepth[2][AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { // 10bpc @@ -710,9 +731,11 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }, }; +#endif // 10/12-bit YUV→RGB LUTs memset(lcf, 0, sizeof(*lcf)); assert(rgb->depth == 8); +#if 0 // Android slim: high-bitdepth libyuv selection unused. if (yuvDepth > 8) { assert(yuvDepth == 10 || yuvDepth == 12); int depthIndex = (yuvDepth == 10) ? 0 : 1; @@ -743,6 +766,10 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, // Fallthrough is intentional. No high bitdepth libyuv function was found. Check if there is an 8-bit libyuv function which // can used with a downshift. } +#endif + if (yuvDepth != 8) { + return AVIF_FALSE; + } if (yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { lcf->yuv400ToRgbMatrix = lutYuv400ToRgbMatrix[rgb->format]; return lcf->yuv400ToRgbMatrix != NULL; @@ -903,6 +930,7 @@ static void getLibYUVConstants(const avifImage * image, const struct YuvConstant } } +#if 0 // Android slim: 10/12-bit downshift unused (8-bit decode only). static avifResult avifImageDownshiftTo8bpc(const avifImage * image, avifImage * image8, avifBool downshiftAlpha) { avifImageSetDefaults(image8); @@ -928,6 +956,7 @@ static avifResult avifImageDownshiftTo8bpc(const avifImage * image, avifImage * } return AVIF_RESULT_OK; } +#endif IGNORE_CFI_ICALL avifResult avifImageYUVToRGBLibYUV(const avifImage * image, avifRGBImage * rgb, avifBool reformatAlpha, avifBool * alphaReformattedWithLibYUV) { @@ -936,7 +965,7 @@ IGNORE_CFI_ICALL avifResult avifImageYUVToRGBLibYUV(const avifImage * image, avi if (image->width > INT_MAX || image->height > INT_MAX || image->yuvRowBytes[AVIF_CHAN_Y] > INT_MAX || rgb->rowBytes > INT_MAX) { return AVIF_RESULT_NOT_IMPLEMENTED; } - if (rgb->depth != 8 || (image->depth != 8 && image->depth != 10 && image->depth != 12)) { + if (rgb->depth != 8 || image->depth != 8) { return AVIF_RESULT_NOT_IMPLEMENTED; } // Find the correct libyuv YuvConstants, based on range and CP/MC @@ -966,143 +995,73 @@ IGNORE_CFI_ICALL avifResult avifImageYUVToRGBLibYUV(const avifImage * image, avi ((rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_FASTEST) || (rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_NEAREST)) ? kFilterNone : kFilterBilinear; - if (lcf.yuvToRgbMatrixFilterHighBitDepth != NULL) { - libyuvResult = lcf.yuvToRgbMatrixFilterHighBitDepth((const uint16_t *)image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y] / 2, - (const uint16_t *)image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex] / 2, - (const uint16_t *)image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex] / 2, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - filter); - } else if (lcf.yuvaToRgbMatrixFilterHighBitDepth != NULL) { - libyuvResult = lcf.yuvaToRgbMatrixFilterHighBitDepth((const uint16_t *)image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y] / 2, - (const uint16_t *)image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex] / 2, - (const uint16_t *)image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex] / 2, - (const uint16_t *)image->alphaPlane, - image->alphaRowBytes / 2, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - /*attenuate=*/0, - filter); - *alphaReformattedWithLibYUV = AVIF_TRUE; - } else if (lcf.yuvToRgbMatrixHighBitDepth != NULL) { - libyuvResult = lcf.yuvToRgbMatrixHighBitDepth((const uint16_t *)image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y] / 2, - (const uint16_t *)image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex] / 2, - (const uint16_t *)image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex] / 2, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height); - } else if (lcf.yuvaToRgbMatrixHighBitDepth != NULL) { - libyuvResult = lcf.yuvaToRgbMatrixHighBitDepth((const uint16_t *)image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y] / 2, - (const uint16_t *)image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex] / 2, - (const uint16_t *)image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex] / 2, - (const uint16_t *)image->alphaPlane, - image->alphaRowBytes / 2, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - /*attentuate=*/0); - *alphaReformattedWithLibYUV = AVIF_TRUE; - } else { - avifImage image8; - avifBool inputIsHighBitDepth = image->depth > 8; - if (inputIsHighBitDepth) { - const avifBool downshiftAlpha = (lcf.yuvaToRgbMatrixFilter != NULL || lcf.yuvaToRgbMatrix != NULL); - AVIF_CHECKRES(avifImageDownshiftTo8bpc(image, &image8, downshiftAlpha)); - image = &image8; - } - if (lcf.yuv400ToRgbMatrix != NULL) { - libyuvResult = lcf.yuv400ToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], + // Android slim: 8-bit YUV420/YUV400 (+ alpha) only; high-bitdepth / downshift paths removed. + if (lcf.yuv400ToRgbMatrix != NULL) { + libyuvResult = lcf.yuv400ToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], + image->yuvRowBytes[AVIF_CHAN_Y], + rgb->pixels, + rgb->rowBytes, + matrix, + image->width, + image->height); + } else if (lcf.yuvToRgbMatrixFilter != NULL) { + libyuvResult = lcf.yuvToRgbMatrixFilter(image->yuvPlanes[AVIF_CHAN_Y], + image->yuvRowBytes[AVIF_CHAN_Y], + image->yuvPlanes[uPlaneIndex], + image->yuvRowBytes[uPlaneIndex], + image->yuvPlanes[vPlaneIndex], + image->yuvRowBytes[vPlaneIndex], + rgb->pixels, + rgb->rowBytes, + matrix, + image->width, + image->height, + filter); + } else if (lcf.yuvaToRgbMatrixFilter != NULL) { + libyuvResult = lcf.yuvaToRgbMatrixFilter(image->yuvPlanes[AVIF_CHAN_Y], image->yuvRowBytes[AVIF_CHAN_Y], + image->yuvPlanes[uPlaneIndex], + image->yuvRowBytes[uPlaneIndex], + image->yuvPlanes[vPlaneIndex], + image->yuvRowBytes[vPlaneIndex], + image->alphaPlane, + image->alphaRowBytes, rgb->pixels, rgb->rowBytes, matrix, image->width, - image->height); - } else if (lcf.yuvToRgbMatrixFilter != NULL) { - libyuvResult = lcf.yuvToRgbMatrixFilter(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex], - image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex], - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - filter); - } else if (lcf.yuvaToRgbMatrixFilter != NULL) { - libyuvResult = lcf.yuvaToRgbMatrixFilter(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex], - image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex], - image->alphaPlane, - image->alphaRowBytes, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - /*attenuate=*/0, - filter); - *alphaReformattedWithLibYUV = AVIF_TRUE; - } else if (lcf.yuvToRgbMatrix != NULL) { - libyuvResult = lcf.yuvToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex], - image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex], - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height); - } else if (lcf.yuvaToRgbMatrix != NULL) { - libyuvResult = lcf.yuvaToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex], - image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex], - image->alphaPlane, - image->alphaRowBytes, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - /*attenuate=*/0); - *alphaReformattedWithLibYUV = AVIF_TRUE; - } - if (inputIsHighBitDepth) { - avifImageFreePlanes(&image8, AVIF_PLANES_ALL); - image = NULL; - } + image->height, + /*attenuate=*/0, + filter); + *alphaReformattedWithLibYUV = AVIF_TRUE; + } else if (lcf.yuvToRgbMatrix != NULL) { + libyuvResult = lcf.yuvToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], + image->yuvRowBytes[AVIF_CHAN_Y], + image->yuvPlanes[uPlaneIndex], + image->yuvRowBytes[uPlaneIndex], + image->yuvPlanes[vPlaneIndex], + image->yuvRowBytes[vPlaneIndex], + rgb->pixels, + rgb->rowBytes, + matrix, + image->width, + image->height); + } else if (lcf.yuvaToRgbMatrix != NULL) { + libyuvResult = lcf.yuvaToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], + image->yuvRowBytes[AVIF_CHAN_Y], + image->yuvPlanes[uPlaneIndex], + image->yuvRowBytes[uPlaneIndex], + image->yuvPlanes[vPlaneIndex], + image->yuvRowBytes[vPlaneIndex], + image->alphaPlane, + image->alphaRowBytes, + rgb->pixels, + rgb->rowBytes, + matrix, + image->width, + image->height, + /*attenuate=*/0); + *alphaReformattedWithLibYUV = AVIF_TRUE; } return (libyuvResult != 0) ? AVIF_RESULT_REFORMAT_FAILED : AVIF_RESULT_OK; } @@ -1162,20 +1121,9 @@ avifResult avifRGBImageUnpremultiplyAlphaLibYUV(avifRGBImage * rgb) avifResult avifRGBImageToF16LibYUV(avifRGBImage * rgb) { - // The width, height, and stride parameters of libyuv functions are all of the int type. - if (rgb->width > INT_MAX || rgb->height > INT_MAX || rgb->rowBytes > INT_MAX) { - return AVIF_RESULT_NOT_IMPLEMENTED; - } - const float scale = 1.0f / ((1 << rgb->depth) - 1); - // Note: HalfFloatPlane requires the stride to be in bytes. - const int result = HalfFloatPlane((const uint16_t *)rgb->pixels, - rgb->rowBytes, - (uint16_t *)rgb->pixels, - rgb->rowBytes, - scale, - rgb->width * avifRGBFormatChannelCount(rgb->format), - rgb->height); - return (result == 0) ? AVIF_RESULT_OK : AVIF_RESULT_INVALID_ARGUMENT; + // Android slim: 8-bit display only; HalfFloatPlane path removed for binary size. + (void)rgb; + return AVIF_RESULT_NOT_IMPLEMENTED; } unsigned int avifLibYUVVersion(void) From 07b949fc935ff015cbf44ddbf9a53d54ff1d07f5 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 16:50:10 +0900 Subject: [PATCH 19/25] =?UTF-8?q?Alpha=20=E3=81=AE=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E3=83=87=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E7=9C=81=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aomedia/avif/android/AvifDecoderTest.java | 3 +- .../src/main/jni/libavif_jni.cc | 62 ++++++++++++++++++- include/avif/avif.h | 8 ++- src/read.c | 30 +++++---- 4 files changed, 84 insertions(+), 19 deletions(-) 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 e8f3060dd1..8e34c85d2f 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 @@ -107,8 +107,9 @@ public ByteBuffer getBuffer() throws IOException { private static final float[] SCALE_FACTORS = {0.5f, 1.3f}; // Matches ext/dav1d_android.sh / LocalDav1d.cmake (-Dbitdepths=8). + // imageCountLimit=1 rejects animated / multi-frame AVIFs at parse time. private boolean isDecodeSupported() { - return image.depth == 8; + return image.depth == 8 && !image.isAnimated; } private static final Image[] IMAGES = { diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index fc4dddb67e..e54c4f72e5 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -232,6 +232,11 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, decoder->decoder->ignoreXMP = AVIF_TRUE; decoder->decoder->ignoreExif = AVIF_TRUE; decoder->decoder->ignoreICC = AVIF_TRUE; + // Still-image only: reject animated / multi-frame AVIFs early to avoid + // allocating per-frame timing and decoder state for sequences. + decoder->decoder->imageCountLimit = 1; + // Start color-only; enable alpha later if the output format needs it. + decoder->decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_COLOR; // Turn off libavif's 'clap' (clean aperture) property validation. This allows // us to detect and ignore streams that have an invalid 'clap' property @@ -342,6 +347,23 @@ avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, return image; } +// Enables alpha decode (and resets) when the output needs it and the file has alpha. +avifResult EnsureAlphaContentIfNeeded(AvifDecoderWrapper* const decoder, + bool need_alpha) { + if (!need_alpha || !decoder->decoder->alphaPresent) { + return AVIF_RESULT_OK; + } + if (decoder->decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_ALPHA) { + return AVIF_RESULT_OK; + } + decoder->decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA; + const avifResult res = avifDecoderReset(decoder->decoder); + if (res != AVIF_RESULT_OK) { + LOGE("Failed to reset decoder for alpha content. Status: %d", res); + } + return res; +} + avifResult AvifImageToRGBBufferFromImage(avifImage* image, void* pixels, size_t row_bytes) { avifRGBImage rgb_image; @@ -546,6 +568,7 @@ bool ShouldUseGray565Output(avifImage* image, bool allow_gray565) { if (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400) { return false; } + // Prefer alphaPresent-equivalent: if alpha was decoded, alphaPlane is set. if (image->alphaPlane != nullptr) { return false; } @@ -690,6 +713,11 @@ jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, uint32_t dst_height = 0; GetTargetDimensions(&decoder, target_width, target_height, &dst_width, &dst_height); + // HardwareBuffer ends as gray565 (no alpha) or RGBA (needs alpha when present). + if (EnsureAlphaContentIfNeeded( + &decoder, decoder.decoder->alphaPresent) != AVIF_RESULT_OK) { + return nullptr; + } if (avifDecoderNextImage(decoder.decoder) != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image for HardwareBuffer output."); return nullptr; @@ -702,6 +730,10 @@ jobject NextFrameToHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, int target_width, int target_height, bool allow_gray565) { + if (EnsureAlphaContentIfNeeded(decoder, decoder->decoder->alphaPresent) != + AVIF_RESULT_OK) { + return nullptr; + } const avifResult decode_result = avifDecoderNextImage(decoder->decoder); if (decode_result != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", decode_result); @@ -719,6 +751,10 @@ jobject NthFrameToHardwareBuffer(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t n, int target_width, int target_height, bool allow_gray565) { + if (EnsureAlphaContentIfNeeded(decoder, decoder->decoder->alphaPresent) != + AVIF_RESULT_OK) { + return nullptr; + } const avifResult decode_result = avifDecoderNthImage(decoder->decoder, n); if (decode_result != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", decode_result); @@ -734,7 +770,18 @@ jobject NthFrameToHardwareBuffer(JNIEnv* const env, avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, jobject bitmap) { - avifResult res = avifDecoderNextImage(decoder->decoder); + AndroidBitmapInfo bitmap_info; + if (AndroidBitmap_getInfo(env, bitmap, &bitmap_info) < 0) { + LOGE("AndroidBitmap_getInfo failed."); + return AVIF_RESULT_UNKNOWN_ERROR; + } + const bool need_alpha = + (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGBA_8888); + avifResult res = EnsureAlphaContentIfNeeded(decoder, need_alpha); + if (res != AVIF_RESULT_OK) { + return res; + } + res = avifDecoderNextImage(decoder->decoder); if (res != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", res); return res; @@ -744,7 +791,18 @@ avifResult DecodeNextImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, avifResult DecodeNthImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, uint32_t n, jobject bitmap) { - avifResult res = avifDecoderNthImage(decoder->decoder, n); + AndroidBitmapInfo bitmap_info; + if (AndroidBitmap_getInfo(env, bitmap, &bitmap_info) < 0) { + LOGE("AndroidBitmap_getInfo failed."); + return AVIF_RESULT_UNKNOWN_ERROR; + } + const bool need_alpha = + (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGBA_8888); + avifResult res = EnsureAlphaContentIfNeeded(decoder, need_alpha); + if (res != AVIF_RESULT_OK) { + return res; + } + res = avifDecoderNthImage(decoder->decoder, n); if (res != AVIF_RESULT_OK) { LOGE("Failed to decode AVIF image. Status: %d", res); return res; diff --git a/include/avif/avif.h b/include/avif/avif.h index faab302089..81d8fd75ee 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -1229,14 +1229,16 @@ AVIF_API const char * avifProgressiveStateToString(avifProgressiveState progress typedef enum avifImageContentTypeFlag { AVIF_IMAGE_CONTENT_NONE = 0, - // Color only or alpha only is not currently supported. - AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA = (1 << 0) | (1 << 1), + AVIF_IMAGE_CONTENT_COLOR = (1 << 0), + AVIF_IMAGE_CONTENT_ALPHA = (1 << 1), + // Decode color and alpha together (default). Color-only is supported; alpha-only is not. + AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA = AVIF_IMAGE_CONTENT_COLOR | AVIF_IMAGE_CONTENT_ALPHA, AVIF_IMAGE_CONTENT_GAIN_MAP = (1 << 2), AVIF_IMAGE_CONTENT_ALL = AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA | AVIF_IMAGE_CONTENT_GAIN_MAP, // Mostly used for bit depth extensions to go beyond the underlying codec capability // (e.g. 16-bit AVIF). Not part of AVIF_IMAGE_CONTENT_ALL as this is a rare use case. - // Has no effect without AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA. + // Has no effect without AVIF_IMAGE_CONTENT_COLOR. AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS = (1 << 3), AVIF_IMAGE_CONTENT_DECODE_DEFAULT = AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA, diff --git a/src/read.c b/src/read.c index ae94a9cc00..a67ecfe2e7 100644 --- a/src/read.c +++ b/src/read.c @@ -5289,10 +5289,10 @@ avifResult avifDecoderParse(avifDecoder * decoder) { avifDiagnosticsClearError(&decoder->diag); - // Color only or alpha only is not currently supported. - if ((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) != 0 && - (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) != AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) { - avifDiagnosticsPrintf(&decoder->diag, "imageContentToDecode set to only color or only alpha is not supported"); + // Alpha-only is not supported. Color-only is allowed (skips alpha decode). + if ((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_ALPHA) && + !(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR)) { + avifDiagnosticsPrintf(&decoder->diag, "imageContentToDecode set to only alpha is not supported"); return AVIF_RESULT_NOT_IMPLEMENTED; } if (!decoder->io || !decoder->io->read) { @@ -6103,10 +6103,10 @@ avifResult avifDecoderReset(avifDecoder * decoder) memset(&decoder->ioStats, 0, sizeof(decoder->ioStats)); - // Color only or alpha only is not currently supported. - if ((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) != 0 && - (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) != AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) { - avifDiagnosticsPrintf(&decoder->diag, "imageContentToDecode set to only color or only alpha is not supported"); + // Alpha-only is not supported. Color-only is allowed (skips alpha decode). + if ((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_ALPHA) && + !(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR)) { + avifDiagnosticsPrintf(&decoder->diag, "imageContentToDecode set to only alpha is not supported"); return AVIF_RESULT_NOT_IMPLEMENTED; } @@ -6236,7 +6236,7 @@ avifResult avifDecoderReset(avifDecoder * decoder) data->diag)); data->tileInfos[AVIF_ITEM_COLOR].tileCount = 1; - if (alphaTrack) { + if (alphaTrack && (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_ALPHA)) { avifTile * alphaTile = avifDecoderDataCreateTile(data, alphaCodecType, alphaTrack->width, alphaTrack->height, operatingPoint); AVIF_CHECKERR(alphaTile != NULL, AVIF_RESULT_OUT_OF_MEMORY); AVIF_CHECKRES(avifCodecDecodeInputFillFromSampleTable(alphaTile->input, @@ -6310,7 +6310,7 @@ avifResult avifDecoderReset(avifDecoder * decoder) &mainItems[AVIF_ITEM_ALPHA], &data->tileInfos[AVIF_ITEM_ALPHA], &isAlphaItemInInput)); - if (mainItems[AVIF_ITEM_ALPHA]) { + if (mainItems[AVIF_ITEM_ALPHA] && (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_ALPHA)) { AVIF_CHECKRES(avifDecoderItemReadAndParse(decoder, mainItems[AVIF_ITEM_ALPHA], isAlphaItemInInput, @@ -6471,13 +6471,17 @@ avifResult avifDecoderReset(avifDecoder * decoder) AVIF_CHECKRES(avifDecoderAdoptGridTileCodecTypeIfNeeded(decoder, mainItems[c], &data->tileInfos[c])); - if (c == AVIF_ITEM_COLOR || c == AVIF_ITEM_ALPHA) { - if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA)) { + if (c == AVIF_ITEM_COLOR) { + if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR)) { + continue; + } + } else if (c == AVIF_ITEM_ALPHA) { + if (!(decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_ALPHA)) { continue; } } else if (c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_COLOR || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_COLOR || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_0_ALPHA || c == AVIF_ITEM_SAMPLE_TRANSFORM_INPUT_1_ALPHA) { - AVIF_ASSERT_OR_RETURN((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR_AND_ALPHA) && + AVIF_ASSERT_OR_RETURN((decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_COLOR) && (decoder->imageContentToDecode & AVIF_IMAGE_CONTENT_SAMPLE_TRANSFORMS)); } else { AVIF_ASSERT_OR_RETURN(c == AVIF_ITEM_GAIN_MAP); From ac65468cd093386b43b00de2d55646e4ffea3b14 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 17:09:43 +0900 Subject: [PATCH 20/25] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E3=83=A1?= =?UTF-8?q?=E3=83=A2=E3=83=AA=E3=81=AE=E9=96=8B=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/aomedia/avif/android/AvifDecoder.java | 12 ++++------ .../avif/android/AvifHardwareDecoder.java | 3 ++- .../src/main/jni/CMakeLists.txt | 8 +------ .../src/main/jni/libavif_jni.cc | 24 ++++++++++--------- include/avif/avif.h | 8 +++++++ src/read.c | 19 +++++++++++++++ 6 files changed, 47 insertions(+), 27 deletions(-) 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 195d242148..92a6708b56 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 @@ -113,10 +113,8 @@ public static boolean decode(ByteBuffer encoded, int length, Bitmap bitmap) { * @param bitmap The decoded pixels will be copied into the bitmap. * If the bitmap dimensions do not match the decoded image's dimensions, * then the decoded image will be scaled to match the bitmap's dimensions. - * @param threads Number of threads to be used for the AVIF decode. Zero means use the library - * determined optimal value as the thread count. Negative values mean use the number of CPU - * cores as the thread count. For more details, see the documentation for maxThreads variable - * in avif.h. + * @param threads Ignored. Decoding always uses a single thread (maxThreads=1) + * to reduce RAM (dav1d worker / scratch buffers). Kept for API compatibility. * @return true on success and false on failure. */ public static native boolean decode(ByteBuffer encoded, int length, Bitmap bitmap, int threads); @@ -189,10 +187,8 @@ public static AvifDecoder create(ByteBuffer encoded) { * * @param encoded The encoded AVIF image. encoded.position() must be 0. The memory of this * ByteBuffer must be kept alive until release() is called. - * @param threads Number of threads to be used by the decoder. Zero means use number of CPU cores - * as the thread count. Negative values are invalid. When this value is > 0, it is simply - * mapped to the maxThreads parameter in libavif. For more details, see the documentation for - * maxThreads variable in avif.h. + * @param threads Ignored. Decoding always uses a single thread (maxThreads=1) + * to reduce RAM. Kept for API compatibility. * @return null on failure. AvifDecoder object on success. */ @Nullable diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java index 27e4a6228f..ae480235e8 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -44,7 +44,8 @@ private AvifHardwareDecoder() {} * also positive. * @param targetHeight Desired output height when scaling; ignored unless {@code targetWidth} is * also positive. - * @param threads Number of threads to be used for the AVIF decode. + * @param threads Ignored. Decoding always uses a single thread (maxThreads=1) + * to reduce RAM. Kept for API compatibility. * @return a HardwareBuffer on success, or null on failure. */ @Nullable diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt index ce022a9327..2607af5538 100644 --- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt +++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt @@ -70,11 +70,5 @@ add_subdirectory(../../../../.. build) add_library("avif_android" SHARED "libavif_jni.cc") -# Import the cpu-features module to compute the number of threads used for -# decoding. -set(CPU_FEATURES_DIR "${ANDROID_NDK}/sources/android/cpufeatures") -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) +target_link_libraries(avif_android jnigraphics avif log) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index e54c4f72e5..b181a1ae79 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -5,12 +5,10 @@ #include #include #include -#include #include #include #include -#include #include #include #include @@ -342,6 +340,15 @@ avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, LOGE("Failed to scale image. Status: %d", *res); return nullptr; } + // avifImageScale() left the source (dav1d / decoder) buffers intact because + // the scaled image was a non-owning view. Drop them now so YUV→RGB only + // retains the smaller scaled planes + the RGB destination. + avifDecoderDropDecodedPlanes(decoder->decoder); + // Cropped/scaling helpers may still hold dangling pointers into the + // dropped buffers; only the owned scaled planes on `image` remain valid. + if (cropped_image && cropped_image.get() != image) { + avifImageFreePlanes(cropped_image.get(), AVIF_PLANES_ALL); + } } *res = AVIF_RESULT_OK; return image; @@ -811,15 +818,10 @@ avifResult DecodeNthImage(JNIEnv* const env, AvifDecoderWrapper* const decoder, } int getThreadCount(int threads) { - if (threads < 0) { - return android_getCpuCount(); - } - if (threads == 0) { - // Empirically, on Android devices with more than 1 core, decoding with 2 - // threads is almost always better than using as many threads as CPU cores. - return std::min(android_getCpuCount(), 2); - } - return threads; + // RAM-oriented Android build: always decode with a single thread to avoid + // dav1d worker / scratch buffers that scale with maxThreads. + (void)threads; + return 1; } // Checks if there is a pending JNI exception that will be thrown when the diff --git a/include/avif/avif.h b/include/avif/avif.h index 81d8fd75ee..86c779e211 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -1440,6 +1440,14 @@ AVIF_API avifResult avifDecoderNextImage(avifDecoder * decoder); AVIF_API avifResult avifDecoderNthImage(avifDecoder * decoder, uint32_t frameIndex); AVIF_API avifResult avifDecoderReset(avifDecoder * decoder); +// Releases pixel buffers for the currently decoded image (owned planes and/or +// codec-backed buffers such as a dav1d picture). Image metadata on +// decoder->image is preserved. Call after you have copied or scaled pixels into +// an independently owned avifImage so the full-size decode buffers can be freed +// before a subsequent YUV→RGB conversion. The next avifDecoderNextImage() / +// avifDecoderNthImage() call will re-decode as needed. +AVIF_API void avifDecoderDropDecodedPlanes(avifDecoder * decoder); + // Keyframe information // frameIndex - 0-based, matching avifDecoder->imageIndex, bound by avifDecoder->imageCount // "nearest" keyframe means the keyframe prior to this frame index (returns frameIndex if it is a keyframe) diff --git a/src/read.c b/src/read.c index a67ecfe2e7..b7fbfbbb70 100644 --- a/src/read.c +++ b/src/read.c @@ -6077,6 +6077,25 @@ static avifResult avifReadCodecConfigProperty(avifImage * image, const avifPrope return AVIF_RESULT_OK; } +void avifDecoderDropDecodedPlanes(avifDecoder * decoder) +{ + if (!decoder) { + return; + } + if (decoder->image) { + avifImageFreePlanes(decoder->image, AVIF_PLANES_ALL); + if (decoder->image->gainMap && decoder->image->gainMap->image) { + avifImageFreePlanes(decoder->image->gainMap->image, AVIF_PLANES_ALL); + } + } + if (decoder->data) { + // Destroys codec instances (e.g. unrefs dav1d pictures) and forgets + // tile plane pointers into those buffers. Codecs are recreated on the + // next avifDecoderNextImage() / avifDecoderNthImage() call. + avifDecoderDataResetCodec(decoder->data); + } +} + avifResult avifDecoderReset(avifDecoder * decoder) { avifDiagnosticsClearError(&decoder->diag); From a4ede21c61d1ed32a75a50ec45f5e7deaa8c49db Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 21:58:15 +0900 Subject: [PATCH 21/25] =?UTF-8?q?RGB565=E3=81=AE=E7=9C=9F=E3=81=AE?= =?UTF-8?q?=E6=96=B9=E3=82=92=E6=B6=88=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android_jni/README.md | 11 ++++++++++- .../org/aomedia/avif/android/AvifDecoderTest.java | 5 +---- .../avifandroidjni/src/main/jni/libavif_jni.cc | 15 ++++----------- include/avif/avif.h | 4 ++-- src/reformat.c | 14 +++++++++++++- src/reformat_libyuv.c | 3 ++- 6 files changed, 32 insertions(+), 20 deletions(-) diff --git a/android_jni/README.md b/android_jni/README.md index d6ee91f1f0..14bbb3c93e 100644 --- a/android_jni/README.md +++ b/android_jni/README.md @@ -24,13 +24,16 @@ $ git clone https://github.com/AOMediaCodec/libavif.git $ cd libavif ``` -Step 2 - Set the SDK and NDK paths in environment variables. (Recommended Android NDK revision: r25c) +Step 2 - Set the SDK and NDK paths in environment variables. (Recommended Android NDK revision: r27c / `27.2.12479018`) ``` $ export ANDROID_SDK_ROOT="/path/to/android/sdk" $ export ANDROID_NDK_HOME="/path/to/android/ndk" ``` +Use a **host-matching** NDK (Linux NDK on Linux/WSL, Darwin NDK on macOS). The Windows +NDK toolchain cannot be used from WSL/Linux. + Step 3 - Checkout and build dav1d (or libgav1) ``` @@ -43,6 +46,9 @@ $ cd .. (`avif` branch). If `ext/dav1d` is absent, the Android JNI CMake LOCAL path fetches the same repository/branch via `LocalDav1d.cmake`. +The script generates meson cross-files targeting **API 21** for all ABIs (required for +NDK r27+, which removed API levels below 21). Override with `ANDROID_API` if needed. + The Android dav1d build is configured with `-Dbitdepths=8` (8-bit AV1 only). Re-run the script after changing this option so that all ABIs are rebuilt. @@ -145,6 +151,9 @@ If any condition fails, or if `AHardwareBuffer_allocate` fails for `R5G6B5`, the back to `RGBA_8888`. (`R5G6B5` is a universally supported HardwareBuffer format from API 26; the caller's API 29 gate for `wrapHardwareBuffer` is sufficient—there is no API 35 requirement.) +`Bitmap.Config.RGB_565` is **not** supported on the software Bitmap decode path; only Gray565 via +`HardwareBuffer` (above) uses the `RGB_565` container. + ### Gray565 packing `RGB_565` here is **not** a true color RGB565 image. It is an 8-bit grayscale value packed into 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 8e34c85d2f..cdb7d6a46f 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 @@ -139,11 +139,8 @@ private boolean isDecodeSupported() { public static List data() throws IOException { ArrayList list = new ArrayList<>(); for (Image image : IMAGES) { - // Test ARGB_8888 for all files. + // Bitmap soft path supports ARGB_8888 only. RGB_565 is Gray565 HardwareBuffer only. list.add(new Object[] {Config.ARGB_8888, image}); - // Test ARGB_8888 and RGB_565 for 8-bit display targets (RGBA_F16 unsupported). - Config testConfig = Config.RGB_565; - list.add(new Object[] {testConfig, image}); } return list; } diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index b181a1ae79..16fa8d028d 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -593,9 +593,9 @@ avifResult AvifImageToBitmap(JNIEnv* const env, LOGE("AndroidBitmap_getInfo failed."); return AVIF_RESULT_UNKNOWN_ERROR; } - // Ensure that the bitmap format is RGBA_8888 or RGB_565 (8-bit display only). - if (bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888 && - bitmap_info.format != ANDROID_BITMAP_FORMAT_RGB_565) { + // Bitmap soft path: RGBA_8888 only. RGB_565 is reserved for YUV400 Gray565 + // HardwareBuffer packing (see PackGray565 / allowGray565), not true-color decode. + if (bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { LOGE("Bitmap format (%d) is not supported.", bitmap_info.format); return AVIF_RESULT_NOT_IMPLEMENTED; } @@ -606,16 +606,9 @@ avifResult AvifImageToBitmap(JNIEnv* const env, return AVIF_RESULT_UNKNOWN_ERROR; } - avifRGBFormat format = AVIF_RGB_FORMAT_RGBA; - int depth = 8; - avifBool is_float = AVIF_FALSE; - if (bitmap_info.format == ANDROID_BITMAP_FORMAT_RGB_565) { - format = AVIF_RGB_FORMAT_RGB_565; - } - const avifResult res = AvifImageToRGBBuffer( decoder, bitmap_pixels, bitmap_info.width, bitmap_info.height, - bitmap_info.stride, format, depth, is_float); + bitmap_info.stride, AVIF_RGB_FORMAT_RGBA, /*depth=*/8, /*is_float=*/AVIF_FALSE); AndroidBitmap_unlockPixels(env, bitmap); return res; } diff --git a/include/avif/avif.h b/include/avif/avif.h index 86c779e211..3c01adcab1 100644 --- a/include/avif/avif.h +++ b/include/avif/avif.h @@ -960,8 +960,8 @@ typedef enum avifRGBFormat // r4 and r0 are the MSB and LSB of the red component respectively. // g5 and g0 are the MSB and LSB of the green component respectively. // b4 and b0 are the MSB and LSB of the blue component respectively. - // This format is only supported for YUV -> RGB conversion and when - // avifRGBImage.depth is set to 8. + // Upstream: YUV -> RGB only, depth 8. Android decode-only builds reject this + // format in avifImageYUVToRGB; monochrome Gray565 packing is JNI-only. AVIF_RGB_FORMAT_RGB_565, AVIF_RGB_FORMAT_GRAY, AVIF_RGB_FORMAT_GRAYA, diff --git a/src/reformat.c b/src/reformat.c index 6832757842..dd8d4c24cc 100644 --- a/src/reformat.c +++ b/src/reformat.c @@ -36,7 +36,12 @@ avifBool avifGetRGBColorSpaceInfo(const avifRGBImage * rgb, avifRGBColorSpaceInf AVIF_CHECK(rgb->depth == 16); } if (rgb->format == AVIF_RGB_FORMAT_RGB_565) { +#if defined(AVIF_DECODE_ONLY) + // Android slim: true-color RGB565 is unused. YUV400 Gray565 packing lives in JNI. + return AVIF_FALSE; +#else AVIF_CHECK(rgb->depth == 8); +#endif } // Cast to silence "comparison of unsigned expression is always true" warning. AVIF_CHECK((int)rgb->format >= AVIF_RGB_FORMAT_RGB && rgb->format < AVIF_RGB_FORMAT_COUNT); @@ -622,10 +627,13 @@ static void avifFreeYUVToRGBLookUpTables(float ** unormFloatTableY, float ** uno *unormFloatTableY = NULL; } +#if !defined(AVIF_DECODE_ONLY) #define RGB565(R, G, B) ((uint16_t)(((B) >> 3) | (((G) >> 2) << 5) | (((R) >> 3) << 11))) +#endif static void avifStoreRGB8Pixel(avifRGBFormat format, uint8_t R, uint8_t G, uint8_t B, uint8_t * ptrR, uint8_t * ptrG, uint8_t * ptrB) { +#if !defined(AVIF_DECODE_ONLY) if (format == AVIF_RGB_FORMAT_RGB_565) { // References for RGB565 color conversion: // * https://docs.microsoft.com/en-us/windows/win32/directshow/working-with-16-bit-rgb @@ -633,6 +641,9 @@ static void avifStoreRGB8Pixel(avifRGBFormat format, uint8_t R, uint8_t G, uint8 *(uint16_t *)ptrR = RGB565(R, G, B); return; } +#else + (void)format; +#endif *ptrR = R; *ptrG = G; *ptrB = B; @@ -1650,7 +1661,8 @@ avifResult avifImageYUVToRGB(const avifImage * image, avifRGBImage * rgb) { #if defined(AVIF_DECODE_ONLY) // Android slim: 8-bit YUV420/YUV400 → 8-bit RGB only (no F16 / 422 / 444 / high bit depth). - if (image->depth != 8 || rgb->depth != 8 || rgb->isFloat || + // RGB_565 true-color conversion is unsupported; YUV400 Gray565 packing is JNI-only. + if (image->depth != 8 || rgb->depth != 8 || rgb->isFloat || rgb->format == AVIF_RGB_FORMAT_RGB_565 || (image->yuvFormat != AVIF_PIXEL_FORMAT_YUV420 && image->yuvFormat != AVIF_PIXEL_FORMAT_YUV400)) { return AVIF_RESULT_NOT_IMPLEMENTED; } diff --git a/src/reformat_libyuv.c b/src/reformat_libyuv.c index 317345185f..12b65177f9 100644 --- a/src/reformat_libyuv.c +++ b/src/reformat_libyuv.c @@ -612,7 +612,8 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, { NULL, NULL, NULL, NULL, NULL }, // BGR { NULL, NULL, NULL, NULL, NULL }, // BGRA { NULL, NULL, NULL, NULL, NULL }, // ABGR - { NULL, NULL, NULL, I420ToRGB565Matrix, NULL }, // RGB_565 + // Android slim: true-color RGB565 unused (YUV400 Gray565 packing is JNI-only). + { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }; // Lookup table for 8-bit YUVA To 8-bit RGB Matrix (4:4:4 or nearest-neighbor filter). From c38b9656d8c8f1a3bde4a9f32a61200c6257aad4 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 22:15:09 +0900 Subject: [PATCH 22/25] =?UTF-8?q?NDK27=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workflows/ci-android-emulator-tests.yml | 6 +- .github/workflows/ci-android-jni.yml | 4 +- android_jni/avifandroidjni/build.gradle | 2 +- cmake/Meson/crossfile-android.meson.in | 15 +++ cmake/Modules/LocalDav1d.cmake | 39 +++++- ext/check_dav1d_syms.sh | 6 +- ext/dav1d_android.sh | 124 +++++++++++++++--- ext/rebuild_dav1d_arm64.sh | 9 +- 8 files changed, 170 insertions(+), 35 deletions(-) create mode 100644 cmake/Meson/crossfile-android.meson.in diff --git a/.github/workflows/ci-android-emulator-tests.yml b/.github/workflows/ci-android-emulator-tests.yml index f560fbef44..4951257d85 100644 --- a/.github/workflows/ci-android-emulator-tests.yml +++ b/.github/workflows/ci-android-emulator-tests.yml @@ -43,8 +43,8 @@ jobs: uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 id: setup-ndk with: - # r25c is the same as 25.2.9519653. - ndk-version: r25c + # r27c is the same as 27.2.12479018. + ndk-version: r27c add-to-path: false - uses: ./.github/actions/setup-linux with: @@ -62,6 +62,6 @@ jobs: working-directory: android_jni api-level: 31 force-avd-creation: false - ndk: 25.2.9519653 + ndk: 27.2.12479018 arch: x86_64 script: ./gradlew cAT diff --git a/.github/workflows/ci-android-jni.yml b/.github/workflows/ci-android-jni.yml index d9186f9691..84a7d116eb 100644 --- a/.github/workflows/ci-android-jni.yml +++ b/.github/workflows/ci-android-jni.yml @@ -24,8 +24,8 @@ jobs: uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1.6.0 id: setup-ndk with: - # r25c is the same as 25.2.9519653. - ndk-version: r25c + # r27c is the same as 27.2.12479018. + ndk-version: r27c add-to-path: false - uses: ./.github/actions/setup-linux with: diff --git a/android_jni/avifandroidjni/build.gradle b/android_jni/avifandroidjni/build.gradle index da7e3e8bdc..1278acf546 100644 --- a/android_jni/avifandroidjni/build.gradle +++ b/android_jni/avifandroidjni/build.gradle @@ -5,7 +5,7 @@ plugins { android { namespace 'org.aomedia.avif.android' compileSdk 31 - ndkVersion "25.2.9519653" + ndkVersion "27.2.12479018" defaultConfig { minSdk 21 diff --git a/cmake/Meson/crossfile-android.meson.in b/cmake/Meson/crossfile-android.meson.in new file mode 100644 index 0000000000..d4b4e05eae --- /dev/null +++ b/cmake/Meson/crossfile-android.meson.in @@ -0,0 +1,15 @@ +[binaries] +c = '@dav1d_android_c@' +cpp = '@dav1d_android_cpp@' +ar = '@dav1d_android_ar@' +strip = '@dav1d_android_strip@' +pkg-config = 'pkg-config' + +[properties] +needs_exe_wrapper = true + +[host_machine] +system = 'android' +cpu_family = '@dav1d_android_cpu_family@' +cpu = '@dav1d_android_cpu@' +endian = 'little' diff --git a/cmake/Modules/LocalDav1d.cmake b/cmake/Modules/LocalDav1d.cmake index c22c749b9c..5dfe158385 100644 --- a/cmake/Modules/LocalDav1d.cmake +++ b/cmake/Modules/LocalDav1d.cmake @@ -36,17 +36,46 @@ function(avif_build_local_dav1d) if(ANDROID) list(APPEND CMAKE_PROGRAM_PATH "${ANDROID_TOOLCHAIN_ROOT}/bin") + # NDK r27+ dropped API levels below 21. Generate a cross-file with API 21 + # instead of dav1d's stock package/crossfiles (x86 still used API 19). + # Matches android_jni minSdk / ext/dav1d_android.sh. + if(NOT DEFINED ANDROID_PLATFORM_LEVEL AND ANDROID_NATIVE_API_LEVEL) + set(ANDROID_PLATFORM_LEVEL ${ANDROID_NATIVE_API_LEVEL}) + endif() + if(NOT ANDROID_PLATFORM_LEVEL OR ANDROID_PLATFORM_LEVEL LESS 21) + set(ANDROID_PLATFORM_LEVEL 21) + endif() + if(CMAKE_SYSTEM_PROCESSOR STREQUAL "armv7-a") - set(android_arch "arm") + set(dav1d_android_cpu_family "arm") + set(dav1d_android_cpu "arm") + set(dav1d_android_clang_prefix "armv7a-linux-androideabi") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64") - set(android_arch "aarch64") + set(dav1d_android_cpu_family "aarch64") + set(dav1d_android_cpu "aarch64") + set(dav1d_android_clang_prefix "aarch64-linux-android") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64") - set(android_arch "x86_64") + set(dav1d_android_cpu_family "x86_64") + set(dav1d_android_cpu "x86_64") + set(dav1d_android_clang_prefix "x86_64-linux-android") else() - set(android_arch "x86") + set(dav1d_android_cpu_family "x86") + set(dav1d_android_cpu "i686") + set(dav1d_android_clang_prefix "i686-linux-android") endif() - set(CROSS_FILE "${source_dir}/package/crossfiles/${android_arch}-android.meson") + set(dav1d_android_toolchain_bin "${ANDROID_TOOLCHAIN_ROOT}/bin") + set(dav1d_android_c + "${dav1d_android_toolchain_bin}/${dav1d_android_clang_prefix}${ANDROID_PLATFORM_LEVEL}-clang" + ) + set(dav1d_android_cpp + "${dav1d_android_toolchain_bin}/${dav1d_android_clang_prefix}${ANDROID_PLATFORM_LEVEL}-clang++" + ) + set(dav1d_android_ar "${dav1d_android_toolchain_bin}/llvm-ar") + set(dav1d_android_strip "${dav1d_android_toolchain_bin}/llvm-strip") + + set(CROSS_FILE "${PROJECT_BINARY_DIR}/crossfile-android-${ANDROID_ABI}.meson") + configure_file("${AVIF_SOURCE_DIR}/cmake/Meson/crossfile-android.meson.in" "${CROSS_FILE}" @ONLY) elseif(APPLE) # If we are cross compiling generate the corresponding file to use with meson if(NOT CMAKE_SYSTEM_PROCESSOR STREQUAL CMAKE_HOST_SYSTEM_PROCESSOR) diff --git a/ext/check_dav1d_syms.sh b/ext/check_dav1d_syms.sh index c026498d11..1b8c3ec7ba 100644 --- a/ext/check_dav1d_syms.sh +++ b/ext/check_dav1d_syms.sh @@ -8,8 +8,10 @@ echo "==== symbols ====" # Prefer llvm-nm from NDK if available NM=nm if command -v llvm-nm >/dev/null 2>&1; then NM=llvm-nm; fi -NDK_NM=/mnt/c/linux/android-sdk/ndk/25.2.9519653/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm -if [ -x "$NDK_NM" ]; then NM="$NDK_NM"; fi +for ndk_ver in 27.2.12479018 27.0.12077973 25.2.9519653; do + NDK_NM=/mnt/c/linux/android-sdk/ndk/${ndk_ver}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-nm + if [ -x "$NDK_NM" ]; then NM="$NDK_NM"; break; fi +done "$NM" src/libdav1d.a 2>/dev/null | grep -E 'obmc_masks|mc_warp_filter|blend_h_8bpc|warp_affine_8x8_8bpc' || echo "(no matching symbols — good if empty for U)" echo "==== undefined count ====" "$NM" src/libdav1d.a 2>/dev/null | grep -E ' U dav1d_obmc_masks| U dav1d_mc_warp_filter' || echo "no undefined obmc/warp refs" diff --git a/ext/dav1d_android.sh b/ext/dav1d_android.sh index d7136f8223..769018bc97 100755 --- a/ext/dav1d_android.sh +++ b/ext/dav1d_android.sh @@ -1,8 +1,13 @@ #!/bin/bash # This script will build dav1d for the default ABI targets supported by android. -# This script only works on linux. You must pass the path to the android NDK as -# a parameter to this script. +# This script works on Linux and macOS. You must pass the path to the android NDK +# as a parameter to this script (host-matching NDK; e.g. the Linux NDK under WSL). +# +# Compatible with Android NDK r27+ (tested with r27 / 27.x). NDK r27 dropped +# API levels below 21, so this script generates meson cross-files that target +# API 21 for every ABI (matches android_jni minSdk) instead of using dav1d's +# stock package/crossfiles (x86 still referenced API 19). # # The build is configured for 8-bit AV1 only (-Dbitdepths=8) to reduce binary # size. 10/12-bit AVIF images will fail to decode with this configuration. @@ -11,31 +16,102 @@ # # Android JNI uses link-u/dav1d (avif branch) rather than upstream videolan. # Feel free to update the branch/commit as needed. +# +# Optional env: +# ANDROID_API - Android API level for the NDK clang wrappers (default: 21) -set -e +set -euo pipefail if [ $# -ne 1 ]; then echo "Usage: ${0} " exit 1 fi -git clone -b avif --depth 1 https://github.com/link-u/dav1d.git -mkdir dav1d/build - -# This only works on linux and mac. -if [ "$(uname)" == "Darwin" ]; then - HOST_TAG="darwin" -else - HOST_TAG="linux" + +NDK="${1}" +API_LEVEL="${ANDROID_API:-21}" + +case "$(uname -s)" in + Darwin) + case "$(uname -m)" in + arm64|aarch64) HOST_TAG="darwin-arm64" ;; + *) HOST_TAG="darwin-x86_64" ;; + esac + ;; + Linux) + HOST_TAG="linux-x86_64" + ;; + *) + echo "Unsupported host OS: $(uname -s) (use Linux or macOS with a matching NDK)" >&2 + exit 1 + ;; +esac + +android_bin="${NDK}/toolchains/llvm/prebuilt/${HOST_TAG}/bin" +if [ ! -d "${android_bin}" ]; then + echo "NDK toolchain not found at: ${android_bin}" >&2 + echo "Pass a host-matching NDK path (Linux NDK on Linux/WSL, Darwin NDK on macOS)." >&2 + exit 1 +fi + +if [ ! -d dav1d ]; then + git clone -b avif --depth 1 https://github.com/link-u/dav1d.git fi -android_bin="${1}/toolchains/llvm/prebuilt/${HOST_TAG}-x86_64/bin" +mkdir -p dav1d/build + +write_cross_file() { + local out="$1" + local cpu_family="$2" + local cpu="$3" + local clang_prefix="$4" + local c_clang="${android_bin}/${clang_prefix}${API_LEVEL}-clang" + local cxx_clang="${android_bin}/${clang_prefix}${API_LEVEL}-clang++" + + if [ ! -x "${c_clang}" ]; then + echo "Compiler not found or not executable: ${c_clang}" >&2 + echo "NDK r27+ requires API >= 21; set ANDROID_API if needed (current: ${API_LEVEL})." >&2 + exit 1 + fi + + cat > "${out}" < aarch64-linux-android21-clang +build_one_abi() { + local abi="$1" + local cpu_family="$2" + local cpu="$3" + local clang_prefix="$4" + local build_dir="dav1d/build/${abi}" + local cross_file="${build_dir}/android-cross.meson" + + mkdir -p "${build_dir}" + write_cross_file "${cross_file}" "${cpu_family}" "${cpu}" "${clang_prefix}" + + local meson_extra=() + if [ -d "${build_dir}/meson-private" ]; then + meson_extra+=(--reconfigure) + fi -ABI_LIST=("armeabi-v7a" "arm64-v8a" "x86" "x86_64") -ARCH_LIST=("arm" "aarch64" "x86" "x86_64") -for i in "${!ABI_LIST[@]}"; do - abi="${ABI_LIST[i]}" # -Db_lto=true so the static archive participates in the Android JNI LTO link. - PATH=$PATH:${android_bin} meson setup --default-library=static --buildtype release \ - --cross-file="dav1d/package/crossfiles/${ARCH_LIST[i]}-android.meson" \ + PATH="${android_bin}:${PATH}" meson setup --default-library=static --buildtype release \ + --cross-file="${cross_file}" \ -Db_lto=true -Dlogging=false -Dbitdepths=8 -Denable_tools=false -Denable_tests=false \ -Denable_docs=false \ -Denable_filmgrain=false \ @@ -44,6 +120,12 @@ for i in "${!ABI_LIST[@]}"; do -Denable_superres=false \ -Denable_warp=false \ -Denable_compound=false \ - "dav1d/build/${abi}" dav1d - PATH=$PATH:${android_bin} meson compile -C "dav1d/build/${abi}" -done + "${meson_extra[@]}" \ + "${build_dir}" dav1d + PATH="${android_bin}:${PATH}" meson compile -C "${build_dir}" +} + +build_one_abi armeabi-v7a arm arm armv7a-linux-androideabi +build_one_abi arm64-v8a aarch64 aarch64 aarch64-linux-android +build_one_abi x86 x86 i686 i686-linux-android +build_one_abi x86_64 x86_64 x86_64 x86_64-linux-android diff --git a/ext/rebuild_dav1d_arm64.sh b/ext/rebuild_dav1d_arm64.sh index cb14e76102..d0198decec 100644 --- a/ext/rebuild_dav1d_arm64.sh +++ b/ext/rebuild_dav1d_arm64.sh @@ -1,6 +1,13 @@ #!/bin/bash set -ex -NDK=/mnt/c/linux/android-sdk/ndk/25.2.9519653 +NDK=/mnt/c/linux/android-sdk/ndk/27.2.12479018 +# Fallback for local r27 installs; prefer r27c when present. +if [ ! -d "$NDK" ]; then + NDK=/mnt/c/linux/android-sdk/ndk/27.0.12077973 +fi +if [ ! -d "$NDK" ]; then + NDK=/mnt/c/linux/android-sdk/ndk/25.2.9519653 +fi export PATH="$PATH:$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" which meson ninja cd /mnt/c/linux/git/libavif/ext/dav1d/build/arm64-v8a From 970bd6b8b35ad0ca859270589864dd86102ff4cf Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sat, 18 Jul 2026 22:45:21 +0900 Subject: [PATCH 23/25] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAAPI=E3=81=AE?= =?UTF-8?q?=E7=B5=9E=E3=82=8A=E8=BE=BC=E3=81=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/androidTest/assets/README.md | 5 +- .../aomedia/avif/android/AvifDecoderTest.java | 3 +- .../avif/android/AvifHardwareDecoder.java | 12 +- .../src/main/jni/libavif_jni.cc | 96 ++------ src/alpha.c | 6 + src/reformat.c | 4 + src/reformat_libyuv.c | 206 ++---------------- src/scale.c | 95 +++----- 8 files changed, 88 insertions(+), 339 deletions(-) diff --git a/android_jni/avifandroidjni/src/androidTest/assets/README.md b/android_jni/avifandroidjni/src/androidTest/assets/README.md index dd5511a3c2..3d7d9106e8 100644 --- a/android_jni/avifandroidjni/src/androidTest/assets/README.md +++ b/android_jni/avifandroidjni/src/androidTest/assets/README.md @@ -9,8 +9,9 @@ Test files for still AVIF decoding. ### blue-and-magenta-crop.avif -Test file with cropping. The image has an encoded size of 320x280. If the Clean -Aperture box is honored, the size of the displayed image will be 180x100. +Test file that includes a Clean Aperture (`clap`) box. Encoded size is 320x280 +(the JNI decoder ignores `clap` and always uses the encoded dimensions; clap +would otherwise display as 180x100). [Source](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/web_tests/images/resources/avif/blue-and-magenta-crop.avif;l=1;drc=3a13337543c1e7de6914f87cd6f02ab06751c572). ## Sub-Directory: animated_avif 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 cdb7d6a46f..dd5c145c5a 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 @@ -127,7 +127,8 @@ private boolean isDecodeSupported() { new Image("avif", "fox.profile2.12bpc.yuv422.avif", 1204, 800, 12, false, 1), new Image("avif", "fox.profile2.12bpc.yuv444.avif", 1204, 800, 12, false, 1), new Image("avif", "fox.profile2.8bpc.yuv422.avif", 1204, 800, 8, false, 1), - new Image("avif", "blue-and-magenta-crop.avif", 180, 100, 8, true, 1), + // Encoded size (clap crop is ignored by the JNI decoder). + new Image("avif", "blue-and-magenta-crop.avif", 320, 280, 8, true, 1), // Parameter ordering for animated images: directory, filename, width, height, depth, // alphaPresent, frameCount, repetitionCount, frameDuration, threads. new Image("animated_avif", "alpha_video.avif", 640, 480, 8, true, 48, -2, 0.04, 1), diff --git a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java index ae480235e8..27f304f3f1 100644 --- a/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -35,7 +35,7 @@ private AvifHardwareDecoder() {} * android.graphics.ColorSpace)}. * *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. - * If either is zero or negative, the cropped image dimensions are used (partial scaling is not + * If either is zero or negative, the encoded image dimensions are used (partial scaling is not * supported). * * @param encoded The encoded AVIF image. encoded.position() must be 0. @@ -91,7 +91,7 @@ public static HardwareBuffer decodeToHardwareBuffer( } /** - * Decodes the AVIF image into an {@link HardwareBuffer} at the cropped image dimensions. + * Decodes the AVIF image into an {@link HardwareBuffer} at the encoded image dimensions. * * @see #decodeToHardwareBuffer(ByteBuffer, int, int, int, int) */ @@ -104,7 +104,7 @@ public static HardwareBuffer decodeToHardwareBuffer(ByteBuffer encoded, int leng * Decodes the next frame of an animated AVIF into an {@link HardwareBuffer}. * *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. - * Otherwise the cropped image dimensions are used. + * Otherwise the encoded image dimensions are used. * * @param decoder A live {@link AvifDecoder} instance created via {@link AvifDecoder#create}. Do * not call {@link AvifDecoder#release()} until this method returns. @@ -132,7 +132,7 @@ public static HardwareBuffer nextFrameHardwareBuffer( decoder.getNativeDecoderHandle(), targetWidth, targetHeight, allowGray565); } - /** Decodes the next frame at the cropped image dimensions. */ + /** Decodes the next frame at the encoded image dimensions. */ @Nullable public static HardwareBuffer nextFrameHardwareBuffer(AvifDecoder decoder) { return nextFrameHardwareBuffer(decoder, 0, 0, false); @@ -142,7 +142,7 @@ public static HardwareBuffer nextFrameHardwareBuffer(AvifDecoder decoder) { * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. * *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. - * Otherwise the cropped image dimensions are used. + * Otherwise the encoded image dimensions are used. * * @param decoder A live {@link AvifDecoder} instance created via {@link AvifDecoder#create}. Do * not call {@link AvifDecoder#release()} until this method returns. @@ -171,7 +171,7 @@ public static HardwareBuffer nthFrameHardwareBuffer( decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight, allowGray565); } - /** Decodes the nth frame at the cropped image dimensions. */ + /** Decodes the nth frame at the encoded image dimensions. */ @Nullable public static HardwareBuffer nthFrameHardwareBuffer(AvifDecoder decoder, int n) { return nthFrameHardwareBuffer(decoder, n, 0, 0, false); diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index 16fa8d028d..a3f53cc01e 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -184,7 +184,6 @@ struct AvifDecoderWrapper { } avifDecoder* decoder = nullptr; - avifCropRect crop; }; // Returns true when `encoded` is a direct ByteBuffer of at least `length` @@ -236,9 +235,8 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, // Start color-only; enable alpha later if the output format needs it. decoder->decoder->imageContentToDecode = AVIF_IMAGE_CONTENT_COLOR; - // Turn off libavif's 'clap' (clean aperture) property validation. This allows - // us to detect and ignore streams that have an invalid 'clap' property - // instead failing. + // Ignore invalid 'clap' (clean aperture); we never apply crop and always use + // the encoded image dimensions. decoder->decoder->strictFlags &= ~AVIF_STRICT_CLAP_VALID; // Allow 'pixi' (pixel information) property to be missing. Older versions of // libheif did not add the 'pixi' item property to AV1 image items (See @@ -255,24 +253,6 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, LOGE("Failed to parse AVIF image: %s.", avifResultToString(res)); return false; } - - avifDiagnostics diag; - // If the image does not have a valid 'clap' property, then we simply display - // the whole image. - // TODO(vigneshv): Handle the case of avifCropRectRequiresUpsampling() - // returning true. - if (!(decoder->decoder->image->transformFlags & AVIF_TRANSFORM_CLAP) || - !avifCropRectFromCleanApertureBox( - &decoder->crop, &decoder->decoder->image->clap, - decoder->decoder->image->width, decoder->decoder->image->height, - &diag) || - avifCropRectRequiresUpsampling(&decoder->crop, - decoder->decoder->image->yuvFormat)) { - decoder->crop.width = decoder->decoder->image->width; - decoder->crop.height = decoder->decoder->image->height; - decoder->crop.x = 0; - decoder->crop.y = 0; - } return true; } @@ -283,57 +263,34 @@ void GetTargetDimensions(AvifDecoderWrapper* const decoder, int target_width, *dst_width = static_cast(target_width); *dst_height = static_cast(target_height); } else { - *dst_width = decoder->crop.width; - *dst_height = decoder->crop.height; + *dst_width = decoder->decoder->image->width; + *dst_height = decoder->decoder->image->height; } } avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, uint32_t dst_width, uint32_t dst_height, avifResult* res, - std::unique_ptr& - cropped_image, std::unique_ptr& scaling_image) { - avifImage* image; - if (decoder->decoder->image->width == decoder->crop.width && - decoder->decoder->image->height == decoder->crop.height && - decoder->crop.x == 0 && decoder->crop.y == 0) { - image = decoder->decoder->image; - } else { - cropped_image.reset(avifImageCreateEmpty()); - if (cropped_image == nullptr) { - LOGE("Failed to allocate cropped image."); + avifImage* image = decoder->decoder->image; + if (image->width != dst_width || image->height != dst_height) { + // Scale a non-owning full-image view so that the decoder image remains + // unchanged. avifImageScale() can read non-owning source planes directly + // and allocates only the destination planes. + scaling_image.reset(avifImageCreateEmpty()); + if (scaling_image == nullptr) { + LOGE("Failed to allocate image view for scaling."); *res = AVIF_RESULT_OUT_OF_MEMORY; return nullptr; } - *res = avifImageSetViewRect(cropped_image.get(), decoder->decoder->image, - &decoder->crop); + const avifCropRect full_image = {0, 0, image->width, image->height}; + *res = avifImageSetViewRect(scaling_image.get(), image, &full_image); if (*res != AVIF_RESULT_OK) { - LOGE("Failed to set crop rectangle. Status: %d", *res); + LOGE("Failed to create image view for scaling. Status: %d", *res); return nullptr; } - image = cropped_image.get(); - } - if (image->width != dst_width || image->height != dst_height) { - if (image == decoder->decoder->image) { - // Scale a non-owning full-image view so that the decoder image remains - // unchanged. avifImageScale() can read non-owning source planes directly - // and allocates only the destination planes. - scaling_image.reset(avifImageCreateEmpty()); - if (scaling_image == nullptr) { - LOGE("Failed to allocate image view for scaling."); - *res = AVIF_RESULT_OUT_OF_MEMORY; - return nullptr; - } - const avifCropRect full_image = {0, 0, image->width, image->height}; - *res = avifImageSetViewRect(scaling_image.get(), image, &full_image); - if (*res != AVIF_RESULT_OK) { - LOGE("Failed to create image view for scaling. Status: %d", *res); - return nullptr; - } - image = scaling_image.get(); - } + image = scaling_image.get(); avifDiagnostics diag; *res = avifImageScale(image, dst_width, dst_height, &diag); if (*res != AVIF_RESULT_OK) { @@ -344,11 +301,6 @@ avifImage* PrepareImageForOutput(AvifDecoderWrapper* const decoder, // the scaled image was a non-owning view. Drop them now so YUV→RGB only // retains the smaller scaled planes + the RGB destination. avifDecoderDropDecodedPlanes(decoder->decoder); - // Cropped/scaling helpers may still hold dangling pointers into the - // dropped buffers; only the owned scaled planes on `image` remain valid. - if (cropped_image && cropped_image.get() != image) { - avifImageFreePlanes(cropped_image.get(), AVIF_PLANES_ALL); - } } *res = AVIF_RESULT_OK; return image; @@ -395,12 +347,10 @@ avifResult AvifImageToRGBBuffer(AvifDecoderWrapper* const decoder, void* pixels, size_t row_bytes, avifRGBFormat format, int depth, avifBool is_float) { avifResult res; - std::unique_ptr cropped_image( - nullptr, avifImageDestroy); std::unique_ptr scaling_image( nullptr, avifImageDestroy); avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, - cropped_image, scaling_image); + scaling_image); if (image == nullptr) { return res; } @@ -625,12 +575,10 @@ jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, } avifResult res; - std::unique_ptr cropped_image( - nullptr, avifImageDestroy); std::unique_ptr scaling_image( nullptr, avifImageDestroy); avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, - cropped_image, scaling_image); + scaling_image); if (image == nullptr) { return nullptr; } @@ -882,9 +830,9 @@ FUNC(jboolean, getInfo, jobject encoded, int length, jobject info) { GET_FIELD_ID(height, info_class, "height", "I", false); GET_FIELD_ID(depth, info_class, "depth", "I", false); GET_FIELD_ID(alpha_present, info_class, "alphaPresent", "Z", false); - env->SetIntField(info, width, decoder.crop.width); + env->SetIntField(info, width, decoder.decoder->image->width); CHECK_EXCEPTION(false); - env->SetIntField(info, height, decoder.crop.height); + env->SetIntField(info, height, decoder.decoder->image->height); CHECK_EXCEPTION(false); env->SetIntField(info, depth, decoder.decoder->image->depth); CHECK_EXCEPTION(false); @@ -934,9 +882,9 @@ FUNC(jlong, createDecoder, jobject encoded, jint length, jint threads) { 0); GET_FIELD_ID(frame_durations_id, avif_decoder_class, "frameDurations", "[D", 0); - env->SetIntField(thiz, width_id, decoder->crop.width); + env->SetIntField(thiz, width_id, decoder->decoder->image->width); CHECK_EXCEPTION(0); - env->SetIntField(thiz, height_id, decoder->crop.height); + env->SetIntField(thiz, height_id, decoder->decoder->image->height); CHECK_EXCEPTION(0); env->SetIntField(thiz, depth_id, decoder->decoder->image->depth); CHECK_EXCEPTION(0); diff --git a/src/alpha.c b/src/alpha.c index 5c9b7a9545..6854a2335e 100644 --- a/src/alpha.c +++ b/src/alpha.c @@ -337,6 +337,11 @@ avifResult avifRGBImagePremultiplyAlpha(avifRGBImage * rgb) avifResult avifRGBImageUnpremultiplyAlpha(avifRGBImage * rgb) { +#if defined(AVIF_DECODE_ONLY) + // Android slim: decode always requests premultiplied RGBA; unpremultiply unused. + (void)rgb; + return AVIF_RESULT_NOT_IMPLEMENTED; +#else // no data if (!rgb->pixels || !rgb->rowBytes) { return AVIF_RESULT_REFORMAT_FAILED; @@ -532,4 +537,5 @@ avifResult avifRGBImageUnpremultiplyAlpha(avifRGBImage * rgb) } return AVIF_RESULT_OK; +#endif // !AVIF_DECODE_ONLY } diff --git a/src/reformat.c b/src/reformat.c index dd8d4c24cc..e5dcbb73cc 100644 --- a/src/reformat.c +++ b/src/reformat.c @@ -1588,11 +1588,13 @@ static avifResult avifImageYUVToRGBImpl(const avifImage * image, avifRGBImage * if (result != AVIF_RESULT_OK) { return result; } +#if !defined(AVIF_DECODE_ONLY) } else if (alphaMultiplyMode == AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY) { avifResult result = avifRGBImageUnpremultiplyAlpha(rgb); if (result != AVIF_RESULT_OK) { return result; } +#endif } // Convert pixels to half floats (F16), if necessary. @@ -1689,8 +1691,10 @@ avifResult avifImageYUVToRGB(const avifImage * image, avifRGBImage * rgb) } else { if (!image->alphaPremultiplied && rgb->alphaPremultiplied) { alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_MULTIPLY; +#if !defined(AVIF_DECODE_ONLY) } else if (image->alphaPremultiplied && !rgb->alphaPremultiplied) { alphaMultiplyMode = AVIF_ALPHA_MULTIPLY_MODE_UNMULTIPLY; +#endif } } } diff --git a/src/reformat_libyuv.c b/src/reformat_libyuv.c index 12b65177f9..545b60bb58 100644 --- a/src/reformat_libyuv.c +++ b/src/reformat_libyuv.c @@ -436,7 +436,7 @@ static const avifBool lutIsYVU[AVIF_RGB_FORMAT_COUNT] = { AVIF_FALSE, // RGB_565 }; -typedef int (*YUV400ToRGBMatrix)(const uint8_t *, int, uint8_t *, int, const struct YuvConstants *, int, int); +// Android slim: YUV420→RGBA bilinear (Filter) only. YUV400 / nearest paths removed. typedef int (*YUVToRGBMatrixFilter)(const uint8_t *, int, const uint8_t *, @@ -464,21 +464,6 @@ typedef int (*YUVAToRGBMatrixFilter)(const uint8_t *, int, int, enum FilterMode); -typedef int (*YUVToRGBMatrix)(const uint8_t *, int, const uint8_t *, int, const uint8_t *, int, uint8_t *, int, const struct YuvConstants *, int, int); -typedef int (*YUVAToRGBMatrix)(const uint8_t *, - int, - const uint8_t *, - int, - const uint8_t *, - int, - const uint8_t *, - int, - uint8_t *, - int, - const struct YuvConstants *, - int, - int, - int); #if 0 // Android slim: high-bitdepth conversion typedefs unused. typedef int (*YUVToRGBMatrixFilterHighBitDepth)(const uint16_t *, int, @@ -537,11 +522,8 @@ typedef int (*YUVAToRGBMatrixHighBitDepth)(const uint16_t *, // At most one pointer in this struct will be not-NULL. typedef struct { - YUV400ToRGBMatrix yuv400ToRgbMatrix; YUVToRGBMatrixFilter yuvToRgbMatrixFilter; YUVAToRGBMatrixFilter yuvaToRgbMatrixFilter; - YUVToRGBMatrix yuvToRgbMatrix; - YUVAToRGBMatrix yuvaToRgbMatrix; #if 0 // Android slim: high-bitdepth function pointers unused. YUVToRGBMatrixFilterHighBitDepth yuvToRgbMatrixFilterHighBitDepth; YUVAToRGBMatrixFilterHighBitDepth yuvaToRgbMatrixFilterHighBitDepth; @@ -550,12 +532,6 @@ typedef struct #endif } LibyuvConversionFunction; -// Only allow nearest-neighbor filter if explicitly specified or left as default. -static avifBool nearestNeighborFilterAllowed(int chromaUpsampling) -{ - return chromaUpsampling != AVIF_CHROMA_UPSAMPLING_BILINEAR && chromaUpsampling != AVIF_CHROMA_UPSAMPLING_BEST_QUALITY; -} - // Returns AVIF_TRUE if the given yuvFormat and yuvDepth can be converted to 8-bit RGB using libyuv, AVIF_FALSE otherwise. When // AVIF_TRUE is returned, exactly one function pointers will be populated with the appropriate conversion function. If // alphaPreferred is set to AVIF_TRUE, then a function that can also copy the alpha channel will be preferred if available. @@ -565,21 +541,8 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, avifBool alphaPreferred, LibyuvConversionFunction * lcf) { - // Lookup table for 8-bit YUV400 to 8-bit RGB Matrix. - // Android slim: RGBA (+ YUV400) only; other RGB layouts unused by JNI. - static const YUV400ToRGBMatrix lutYuv400ToRgbMatrix[AVIF_RGB_FORMAT_COUNT] = { - // // AVIF_RGB_FORMAT_ - NULL, // RGB - I400ToARGBMatrix, // RGBA - NULL, // ARGB - NULL, // BGR - NULL, // BGRA - NULL, // ABGR - NULL, // RGB_565 - }; - - // Lookup table for 8-bit YUV To 8-bit RGB Matrix (with filter). - // Android slim: YUV420 + RGBA only. + // Android slim: 8-bit YUV420 → RGBA bilinear only. + // YUV400 uses JNI Gray565 (or built-in mono→RGBA); nearest / non-Filter paths removed. static const YUVToRGBMatrixFilter lutYuvToRgbMatrixFilter[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ { NULL, NULL, NULL, NULL, NULL }, // RGB @@ -591,7 +554,6 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }; - // Lookup table for 8-bit YUVA To 8-bit RGB Matrix (with filter). static const YUVAToRGBMatrixFilter lutYuvaToRgbMatrixFilter[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ { NULL, NULL, NULL, NULL, NULL }, // RGB @@ -603,31 +565,6 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, { NULL, NULL, NULL, NULL, NULL }, // RGB_565 }; - // Lookup table for 8-bit YUV To 8-bit RGB Matrix (4:4:4 or nearest-neighbor filter). - static const YUVToRGBMatrix lutYuvToRgbMatrix[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { - // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ - { NULL, NULL, NULL, NULL, NULL }, // RGB - { NULL, NULL, NULL, I420ToARGBMatrix, NULL }, // RGBA - { NULL, NULL, NULL, NULL, NULL }, // ARGB - { NULL, NULL, NULL, NULL, NULL }, // BGR - { NULL, NULL, NULL, NULL, NULL }, // BGRA - { NULL, NULL, NULL, NULL, NULL }, // ABGR - // Android slim: true-color RGB565 unused (YUV400 Gray565 packing is JNI-only). - { NULL, NULL, NULL, NULL, NULL }, // RGB_565 - }; - - // Lookup table for 8-bit YUVA To 8-bit RGB Matrix (4:4:4 or nearest-neighbor filter). - static const YUVAToRGBMatrix lutYuvaToRgbMatrix[AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { - // { NONE, YUV444, YUV422, YUV420, YUV400 } // AVIF_RGB_FORMAT_ - { NULL, NULL, NULL, NULL, NULL }, // RGB - { NULL, NULL, NULL, I420AlphaToARGBMatrix, NULL }, // RGBA - { NULL, NULL, NULL, NULL, NULL }, // ARGB - { NULL, NULL, NULL, NULL, NULL }, // BGR - { NULL, NULL, NULL, NULL, NULL }, // BGRA - { NULL, NULL, NULL, NULL, NULL }, // ABGR - { NULL, NULL, NULL, NULL, NULL }, // RGB_565 - }; - #if 0 // Android slim: 10/12-bit libyuv YUV→RGB paths unused (8-bit decode only). // Lookup table for YUV To RGB Matrix (with filter). First dimension is for the YUV bit depth. static const YUVToRGBMatrixFilterHighBitDepth lutYuvToRgbMatrixFilterHighBitDepth[2][AVIF_RGB_FORMAT_COUNT][AVIF_PIXEL_FORMAT_COUNT] = { @@ -736,77 +673,22 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, memset(lcf, 0, sizeof(*lcf)); assert(rgb->depth == 8); -#if 0 // Android slim: high-bitdepth libyuv selection unused. - if (yuvDepth > 8) { - assert(yuvDepth == 10 || yuvDepth == 12); - int depthIndex = (yuvDepth == 10) ? 0 : 1; - if (yuvFormat != AVIF_PIXEL_FORMAT_YUV444) { - if (alphaPreferred) { - lcf->yuvaToRgbMatrixFilterHighBitDepth = lutYuvaToRgbMatrixFilterHighBitDepth[depthIndex][rgb->format][yuvFormat]; - if (lcf->yuvaToRgbMatrixFilterHighBitDepth != NULL) { - return AVIF_TRUE; - } - } - lcf->yuvToRgbMatrixFilterHighBitDepth = lutYuvToRgbMatrixFilterHighBitDepth[depthIndex][rgb->format][yuvFormat]; - if (lcf->yuvToRgbMatrixFilterHighBitDepth != NULL) { - return AVIF_TRUE; - } - } - if (yuvFormat == AVIF_PIXEL_FORMAT_YUV444 || nearestNeighborFilterAllowed(rgb->chromaUpsampling)) { - if (alphaPreferred) { - lcf->yuvaToRgbMatrixHighBitDepth = lutYuvaToRgbMatrixHighBitDepth[depthIndex][rgb->format][yuvFormat]; - if (lcf->yuvaToRgbMatrixHighBitDepth != NULL) { - return AVIF_TRUE; - } - } - lcf->yuvToRgbMatrixHighBitDepth = lutYuvToRgbMatrixHighBitDepth[depthIndex][rgb->format][yuvFormat]; - if (lcf->yuvToRgbMatrixHighBitDepth != NULL) { - return AVIF_TRUE; - } - } - // Fallthrough is intentional. No high bitdepth libyuv function was found. Check if there is an 8-bit libyuv function which - // can used with a downshift. - } -#endif - if (yuvDepth != 8) { + if (yuvDepth != 8 || yuvFormat != AVIF_PIXEL_FORMAT_YUV420) { return AVIF_FALSE; } - if (yuvFormat == AVIF_PIXEL_FORMAT_YUV400) { - lcf->yuv400ToRgbMatrix = lutYuv400ToRgbMatrix[rgb->format]; - return lcf->yuv400ToRgbMatrix != NULL; - } - if (yuvFormat != AVIF_PIXEL_FORMAT_YUV444) { - if (alphaPreferred) { - lcf->yuvaToRgbMatrixFilter = lutYuvaToRgbMatrixFilter[rgb->format][yuvFormat]; - if (lcf->yuvaToRgbMatrixFilter != NULL) { - return AVIF_TRUE; - } - } - lcf->yuvToRgbMatrixFilter = lutYuvToRgbMatrixFilter[rgb->format][yuvFormat]; - if (lcf->yuvToRgbMatrixFilter != NULL) { - return AVIF_TRUE; - } - if (!nearestNeighborFilterAllowed(rgb->chromaUpsampling)) { - return AVIF_FALSE; - } - } if (alphaPreferred) { - lcf->yuvaToRgbMatrix = lutYuvaToRgbMatrix[rgb->format][yuvFormat]; - if (lcf->yuvaToRgbMatrix != NULL) { + lcf->yuvaToRgbMatrixFilter = lutYuvaToRgbMatrixFilter[rgb->format][yuvFormat]; + if (lcf->yuvaToRgbMatrixFilter != NULL) { return AVIF_TRUE; } } - lcf->yuvToRgbMatrix = lutYuvToRgbMatrix[rgb->format][yuvFormat]; - return lcf->yuvToRgbMatrix != NULL; + lcf->yuvToRgbMatrixFilter = lutYuvToRgbMatrixFilter[rgb->format][yuvFormat]; + return lcf->yuvToRgbMatrixFilter != NULL; } static void getLibYUVConstants(const avifImage * image, const struct YuvConstants ** matrixYUV, const struct YuvConstants ** matrixYVU) { - // Allow the identity matrix to be used with YUV 4:0:0. Replace the identity matrix with - // MatrixCoefficients 6 (BT.601). - const avifBool yuv400WithIdentityMatrix = (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && - (image->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY); - const avifMatrixCoefficients matrixCoefficients = yuv400WithIdentityMatrix ? AVIF_MATRIX_COEFFICIENTS_BT601 : image->matrixCoefficients; + const avifMatrixCoefficients matrixCoefficients = image->matrixCoefficients; if (image->yuvRange == AVIF_RANGE_FULL) { switch (matrixCoefficients) { // BT.709 full range YuvConstants were added in libyuv version 1772. @@ -992,20 +874,8 @@ IGNORE_CFI_ICALL avifResult avifImageYUVToRGBLibYUV(const avifImage * image, avi int libyuvResult = -1; int uPlaneIndex = isYVU ? AVIF_CHAN_V : AVIF_CHAN_U; int vPlaneIndex = isYVU ? AVIF_CHAN_U : AVIF_CHAN_V; - const enum FilterMode filter = - ((rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_FASTEST) || (rgb->chromaUpsampling == AVIF_CHROMA_UPSAMPLING_NEAREST)) - ? kFilterNone - : kFilterBilinear; - // Android slim: 8-bit YUV420/YUV400 (+ alpha) only; high-bitdepth / downshift paths removed. - if (lcf.yuv400ToRgbMatrix != NULL) { - libyuvResult = lcf.yuv400ToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height); - } else if (lcf.yuvToRgbMatrixFilter != NULL) { + // Android slim: YUV420→RGBA bilinear only (YUV400 / nearest / high-bitdepth removed). + if (lcf.yuvToRgbMatrixFilter != NULL) { libyuvResult = lcf.yuvToRgbMatrixFilter(image->yuvPlanes[AVIF_CHAN_Y], image->yuvRowBytes[AVIF_CHAN_Y], image->yuvPlanes[uPlaneIndex], @@ -1017,7 +887,7 @@ IGNORE_CFI_ICALL avifResult avifImageYUVToRGBLibYUV(const avifImage * image, avi matrix, image->width, image->height, - filter); + kFilterBilinear); } else if (lcf.yuvaToRgbMatrixFilter != NULL) { libyuvResult = lcf.yuvaToRgbMatrixFilter(image->yuvPlanes[AVIF_CHAN_Y], image->yuvRowBytes[AVIF_CHAN_Y], @@ -1033,35 +903,7 @@ IGNORE_CFI_ICALL avifResult avifImageYUVToRGBLibYUV(const avifImage * image, avi image->width, image->height, /*attenuate=*/0, - filter); - *alphaReformattedWithLibYUV = AVIF_TRUE; - } else if (lcf.yuvToRgbMatrix != NULL) { - libyuvResult = lcf.yuvToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex], - image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex], - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height); - } else if (lcf.yuvaToRgbMatrix != NULL) { - libyuvResult = lcf.yuvaToRgbMatrix(image->yuvPlanes[AVIF_CHAN_Y], - image->yuvRowBytes[AVIF_CHAN_Y], - image->yuvPlanes[uPlaneIndex], - image->yuvRowBytes[uPlaneIndex], - image->yuvPlanes[vPlaneIndex], - image->yuvRowBytes[vPlaneIndex], - image->alphaPlane, - image->alphaRowBytes, - rgb->pixels, - rgb->rowBytes, - matrix, - image->width, - image->height, - /*attenuate=*/0); + kFilterBilinear); *alphaReformattedWithLibYUV = AVIF_TRUE; } return (libyuvResult != 0) ? AVIF_RESULT_REFORMAT_FAILED : AVIF_RESULT_OK; @@ -1097,26 +939,8 @@ avifResult avifRGBImagePremultiplyAlphaLibYUV(avifRGBImage * rgb) avifResult avifRGBImageUnpremultiplyAlphaLibYUV(avifRGBImage * rgb) { - // See if the current settings can be accomplished with libyuv, and use it (if possible). - - // The width, height, and stride parameters of libyuv functions are all of the int type. - if (rgb->width > INT_MAX || rgb->height > INT_MAX || rgb->rowBytes > INT_MAX) { - return AVIF_RESULT_NOT_IMPLEMENTED; - } - if (rgb->depth != 8) { - return AVIF_RESULT_NOT_IMPLEMENTED; - } - - // libavif uses byte-order when describing pixel formats, such that the R in RGBA is the lowest address, - // similar to PNG. libyuv orders in word-order, so libavif's RGBA would be referred to in libyuv as ABGR. - - if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) { - if (ARGBUnattenuate(rgb->pixels, rgb->rowBytes, rgb->pixels, rgb->rowBytes, rgb->width, rgb->height) != 0) { - return AVIF_RESULT_REFORMAT_FAILED; - } - return AVIF_RESULT_OK; - } - + // Android slim: decode path always requests premultiplied RGBA; unpremultiply unused. + (void)rgb; return AVIF_RESULT_NOT_IMPLEMENTED; } diff --git a/src/scale.c b/src/scale.c index 30cbf9a74b..33db09da3d 100644 --- a/src/scale.c +++ b/src/scale.c @@ -43,6 +43,12 @@ avifResult avifImageScaleWithLimit(avifImage * image, return AVIF_RESULT_NOT_IMPLEMENTED; } + // Android slim: 8-bit planes only (no ScalePlane_12 / ScalePlane_16). + if (image->depth != 8) { + avifDiagnosticsPrintf(diag, "avifImageScaleWithLimit supports 8-bit images only (depth %u)", image->depth); + return AVIF_RESULT_NOT_IMPLEMENTED; + } + uint8_t * srcYUVPlanes[AVIF_PLANE_COUNT_YUV]; uint32_t srcYUVRowBytes[AVIF_PLANE_COUNT_YUV]; for (int i = 0; i < AVIF_PLANE_COUNT_YUV; ++i) { @@ -70,8 +76,7 @@ avifResult avifImageScaleWithLimit(avifImage * image, avifResult result = AVIF_RESULT_OK; if (srcYUVPlanes[0] || srcAlphaPlane) { - // A simple conservative check to avoid integer overflows in libyuv's ScalePlane() and - // ScalePlane_12() functions. + // A simple conservative check to avoid integer overflows in libyuv's ScalePlane(). if (srcWidth > 16384) { avifDiagnosticsPrintf(diag, "avifImageScaleWithLimit requested invalid width scale for libyuv [%u -> %u]", srcWidth, dstWidth); result = AVIF_RESULT_NOT_IMPLEMENTED; @@ -101,40 +106,20 @@ avifResult avifImageScaleWithLimit(avifImage * image, const uint32_t srcH = (i == AVIF_CHAN_Y) ? srcHeight : srcUVHeight; const uint32_t dstW = avifImagePlaneWidth(image, i); const uint32_t dstH = avifImagePlaneHeight(image, i); - if (image->depth > 8) { - uint16_t * const srcPlane = (uint16_t *)srcYUVPlanes[i]; - const uint32_t srcStride = srcYUVRowBytes[i] / 2; - uint16_t * const dstPlane = (uint16_t *)image->yuvPlanes[i]; - const uint32_t dstStride = image->yuvRowBytes[i] / 2; -#if LIBYUV_VERSION >= 1880 - const int failure = - ScalePlane_12(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); - if (failure) { - avifDiagnosticsPrintf(diag, "ScalePlane_12() failed (%d)", failure); - result = (failure == 1) ? AVIF_RESULT_OUT_OF_MEMORY : AVIF_RESULT_UNKNOWN_ERROR; - goto cleanup; - } -#elif LIBYUV_VERSION >= 1774 - ScalePlane_12(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); -#else - ScalePlane_16(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); -#endif - } else { - uint8_t * const srcPlane = srcYUVPlanes[i]; - const uint32_t srcStride = srcYUVRowBytes[i]; - uint8_t * const dstPlane = image->yuvPlanes[i]; - const uint32_t dstStride = image->yuvRowBytes[i]; + uint8_t * const srcPlane = srcYUVPlanes[i]; + const uint32_t srcStride = srcYUVRowBytes[i]; + uint8_t * const dstPlane = image->yuvPlanes[i]; + const uint32_t dstStride = image->yuvRowBytes[i]; #if LIBYUV_VERSION >= 1880 - const int failure = ScalePlane(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); - if (failure) { - avifDiagnosticsPrintf(diag, "ScalePlane() failed (%d)", failure); - result = (failure == 1) ? AVIF_RESULT_OUT_OF_MEMORY : AVIF_RESULT_UNKNOWN_ERROR; - goto cleanup; - } + const int failure = ScalePlane(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); + if (failure) { + avifDiagnosticsPrintf(diag, "ScalePlane() failed (%d)", failure); + result = (failure == 1) ? AVIF_RESULT_OUT_OF_MEMORY : AVIF_RESULT_UNKNOWN_ERROR; + goto cleanup; + } #else - ScalePlane(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); + ScalePlane(srcPlane, srcStride, srcW, srcH, dstPlane, dstStride, dstW, dstH, AVIF_LIBYUV_FILTER_MODE); #endif - } } } @@ -146,41 +131,21 @@ avifResult avifImageScaleWithLimit(avifImage * image, goto cleanup; } - if (image->depth > 8) { - uint16_t * const srcPlane = (uint16_t *)srcAlphaPlane; - const uint32_t srcStride = srcAlphaRowBytes / 2; - uint16_t * const dstPlane = (uint16_t *)image->alphaPlane; - const uint32_t dstStride = image->alphaRowBytes / 2; + uint8_t * const srcPlane = srcAlphaPlane; + const uint32_t srcStride = srcAlphaRowBytes; + uint8_t * const dstPlane = image->alphaPlane; + const uint32_t dstStride = image->alphaRowBytes; #if LIBYUV_VERSION >= 1880 - const int failure = - ScalePlane_12(srcPlane, srcStride, srcWidth, srcHeight, dstPlane, dstStride, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE); - if (failure) { - avifDiagnosticsPrintf(diag, "ScalePlane_12() failed (%d)", failure); - result = (failure == 1) ? AVIF_RESULT_OUT_OF_MEMORY : AVIF_RESULT_UNKNOWN_ERROR; - goto cleanup; - } -#elif LIBYUV_VERSION >= 1774 - ScalePlane_12(srcPlane, srcStride, srcWidth, srcHeight, dstPlane, dstStride, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE); -#else - ScalePlane_16(srcPlane, srcStride, srcWidth, srcHeight, dstPlane, dstStride, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE); -#endif - } else { - uint8_t * const srcPlane = srcAlphaPlane; - const uint32_t srcStride = srcAlphaRowBytes; - uint8_t * const dstPlane = image->alphaPlane; - const uint32_t dstStride = image->alphaRowBytes; -#if LIBYUV_VERSION >= 1880 - const int failure = - ScalePlane(srcPlane, srcStride, srcWidth, srcHeight, dstPlane, dstStride, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE); - if (failure) { - avifDiagnosticsPrintf(diag, "ScalePlane() failed (%d)", failure); - result = (failure == 1) ? AVIF_RESULT_OUT_OF_MEMORY : AVIF_RESULT_UNKNOWN_ERROR; - goto cleanup; - } -#else + const int failure = ScalePlane(srcPlane, srcStride, srcWidth, srcHeight, dstPlane, dstStride, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE); -#endif + if (failure) { + avifDiagnosticsPrintf(diag, "ScalePlane() failed (%d)", failure); + result = (failure == 1) ? AVIF_RESULT_OUT_OF_MEMORY : AVIF_RESULT_UNKNOWN_ERROR; + goto cleanup; } +#else + ScalePlane(srcPlane, srcStride, srcWidth, srcHeight, dstPlane, dstStride, dstWidth, dstHeight, AVIF_LIBYUV_FILTER_MODE); +#endif } cleanup: From 37d6b9e232a7221e7d99fcafd48db8e6e985ea59 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sun, 19 Jul 2026 12:31:41 +0900 Subject: [PATCH 24/25] BugFix --- src/reformat_libyuv.c | 147 +++++++----------------------------------- 1 file changed, 25 insertions(+), 122 deletions(-) diff --git a/src/reformat_libyuv.c b/src/reformat_libyuv.c index 545b60bb58..8b12c35fe5 100644 --- a/src/reformat_libyuv.c +++ b/src/reformat_libyuv.c @@ -688,128 +688,31 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, static void getLibYUVConstants(const avifImage * image, const struct YuvConstants ** matrixYUV, const struct YuvConstants ** matrixYVU) { - const avifMatrixCoefficients matrixCoefficients = image->matrixCoefficients; - if (image->yuvRange == AVIF_RANGE_FULL) { - switch (matrixCoefficients) { - // BT.709 full range YuvConstants were added in libyuv version 1772. - // See https://chromium-review.googlesource.com/c/libyuv/libyuv/+/2646472. - case AVIF_MATRIX_COEFFICIENTS_BT709: -#if LIBYUV_VERSION >= 1772 - *matrixYUV = &kYuvF709Constants; - *matrixYVU = &kYvuF709Constants; -#endif - break; - case AVIF_MATRIX_COEFFICIENTS_BT470BG: - case AVIF_MATRIX_COEFFICIENTS_BT601: - case AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED: - *matrixYUV = &kYuvJPEGConstants; - *matrixYVU = &kYvuJPEGConstants; - break; - // BT.2020 full range YuvConstants were added in libyuv version 1775. - // See https://chromium-review.googlesource.com/c/libyuv/libyuv/+/2678859. - case AVIF_MATRIX_COEFFICIENTS_BT2020_NCL: -#if LIBYUV_VERSION >= 1775 - *matrixYUV = &kYuvV2020Constants; - *matrixYVU = &kYvuV2020Constants; -#endif - break; - case AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL: - switch (image->colorPrimaries) { - case AVIF_COLOR_PRIMARIES_BT709: - case AVIF_COLOR_PRIMARIES_UNSPECIFIED: -#if LIBYUV_VERSION >= 1772 - *matrixYUV = &kYuvF709Constants; - *matrixYVU = &kYvuF709Constants; -#endif - break; - case AVIF_COLOR_PRIMARIES_BT470BG: - case AVIF_COLOR_PRIMARIES_BT601: - *matrixYUV = &kYuvJPEGConstants; - *matrixYVU = &kYvuJPEGConstants; - break; - case AVIF_COLOR_PRIMARIES_BT2020: -#if LIBYUV_VERSION >= 1775 - *matrixYUV = &kYuvV2020Constants; - *matrixYVU = &kYvuV2020Constants; -#endif - break; - - case AVIF_COLOR_PRIMARIES_UNKNOWN: - case AVIF_COLOR_PRIMARIES_BT470M: - case AVIF_COLOR_PRIMARIES_SMPTE240: - case AVIF_COLOR_PRIMARIES_GENERIC_FILM: - case AVIF_COLOR_PRIMARIES_XYZ: - case AVIF_COLOR_PRIMARIES_SMPTE431: - case AVIF_COLOR_PRIMARIES_SMPTE432: - case AVIF_COLOR_PRIMARIES_EBU3213: - break; - } - break; - - case AVIF_MATRIX_COEFFICIENTS_IDENTITY: - case AVIF_MATRIX_COEFFICIENTS_FCC: - case AVIF_MATRIX_COEFFICIENTS_SMPTE240: - case AVIF_MATRIX_COEFFICIENTS_YCGCO: - case AVIF_MATRIX_COEFFICIENTS_BT2020_CL: - case AVIF_MATRIX_COEFFICIENTS_SMPTE2085: - case AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_CL: - case AVIF_MATRIX_COEFFICIENTS_ICTCP: - break; - } - } else { // image->yuvRange == AVIF_RANGE_LIMITED - switch (matrixCoefficients) { - case AVIF_MATRIX_COEFFICIENTS_BT709: - *matrixYUV = &kYuvH709Constants; - *matrixYVU = &kYvuH709Constants; - break; - case AVIF_MATRIX_COEFFICIENTS_BT470BG: - case AVIF_MATRIX_COEFFICIENTS_BT601: - case AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED: - *matrixYUV = &kYuvI601Constants; - *matrixYVU = &kYvuI601Constants; - break; - case AVIF_MATRIX_COEFFICIENTS_BT2020_NCL: - *matrixYUV = &kYuv2020Constants; - *matrixYVU = &kYvu2020Constants; - break; - case AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL: - switch (image->colorPrimaries) { - case AVIF_COLOR_PRIMARIES_BT709: - case AVIF_COLOR_PRIMARIES_UNSPECIFIED: - *matrixYUV = &kYuvH709Constants; - *matrixYVU = &kYvuH709Constants; - break; - case AVIF_COLOR_PRIMARIES_BT470BG: - case AVIF_COLOR_PRIMARIES_BT601: - *matrixYUV = &kYuvI601Constants; - *matrixYVU = &kYvuI601Constants; - break; - case AVIF_COLOR_PRIMARIES_BT2020: - *matrixYUV = &kYuv2020Constants; - *matrixYVU = &kYvu2020Constants; - break; - - case AVIF_COLOR_PRIMARIES_UNKNOWN: - case AVIF_COLOR_PRIMARIES_BT470M: - case AVIF_COLOR_PRIMARIES_SMPTE240: - case AVIF_COLOR_PRIMARIES_GENERIC_FILM: - case AVIF_COLOR_PRIMARIES_XYZ: - case AVIF_COLOR_PRIMARIES_SMPTE431: - case AVIF_COLOR_PRIMARIES_SMPTE432: - case AVIF_COLOR_PRIMARIES_EBU3213: - break; - } - break; - case AVIF_MATRIX_COEFFICIENTS_IDENTITY: - case AVIF_MATRIX_COEFFICIENTS_FCC: - case AVIF_MATRIX_COEFFICIENTS_SMPTE240: - case AVIF_MATRIX_COEFFICIENTS_YCGCO: - case AVIF_MATRIX_COEFFICIENTS_BT2020_CL: - case AVIF_MATRIX_COEFFICIENTS_SMPTE2085: - case AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_CL: - case AVIF_MATRIX_COEFFICIENTS_ICTCP: - break; - } + // Android slim: linked libyuv only provides BT.709 limited (kYuv/kYvuH709Constants). + // Other MC/range combinations fall back to libavif's C conversion path. + *matrixYUV = NULL; + *matrixYVU = NULL; + if (image->yuvRange != AVIF_RANGE_LIMITED) { + return; + } + switch (image->matrixCoefficients) { + case AVIF_MATRIX_COEFFICIENTS_BT709: + *matrixYUV = &kYuvH709Constants; + *matrixYVU = &kYvuH709Constants; + break; + case AVIF_MATRIX_COEFFICIENTS_CHROMA_DERIVED_NCL: + switch (image->colorPrimaries) { + case AVIF_COLOR_PRIMARIES_BT709: + case AVIF_COLOR_PRIMARIES_UNSPECIFIED: + *matrixYUV = &kYuvH709Constants; + *matrixYVU = &kYvuH709Constants; + break; + default: + break; + } + break; + default: + break; } } From 2cddfd5fe32f3f4a27f7dfd8b6c9752781895f28 Mon Sep 17 00:00:00 2001 From: Tsuyoshi Yamada Date: Sun, 19 Jul 2026 15:28:25 +0900 Subject: [PATCH 25/25] BugFix --- .../src/main/jni/libavif_jni.cc | 163 ++++++++++++++---- 1 file changed, 134 insertions(+), 29 deletions(-) diff --git a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc index a3f53cc01e..80705fee3e 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -401,6 +401,10 @@ struct Gray565LutTable { uint16_t data[256]; }; +struct LimitedToFullLutTable { + uint8_t data[256]; +}; + constexpr Gray565LutTable MakeGray565LutFull() { Gray565LutTable table = {}; for (int v = 0; v < 256; ++v) { @@ -417,8 +421,17 @@ constexpr Gray565LutTable MakeGray565LutLimited() { return table; } +constexpr LimitedToFullLutTable MakeLimitedToFullLut() { + LimitedToFullLutTable table = {}; + for (int v = 0; v < 256; ++v) { + table.data[v] = LimitedToFull8(v); + } + return table; +} + constexpr Gray565LutTable kGray565LutFull = MakeGray565LutFull(); constexpr Gray565LutTable kGray565LutLimited = MakeGray565LutLimited(); +constexpr LimitedToFullLutTable kLimitedToFullLut = MakeLimitedToFullLut(); const uint16_t* Gray565Lut(avifRange range) { return range == AVIF_RANGE_LIMITED ? kGray565LutLimited.data @@ -427,55 +440,142 @@ const uint16_t* Gray565Lut(avifRange range) { #if defined(__ARM_NEON) || defined(__ARM_NEON__) // Pack 8 full-range Y samples: R5=Y[1:0], G6=Y[7:2], B5=0. -static uint16x8_t PackGray565Neon(uint16x8_t y) { - const uint16x8_t lo = vandq_u16(y, vdupq_n_u16(3)); - const uint16x8_t hi = vshrq_n_u16(y, 2); - return vorrq_u16(vshlq_n_u16(lo, 11), vshlq_n_u16(hi, 5)); +static inline uint16x8_t PackGray565Neon(uint16x8_t y) { + const uint16x8_t g = vshlq_n_u16(vshrq_n_u16(y, 2), 5); + return vsliq_n_u16(g, vandq_u16(y, vdupq_n_u16(3)), 11); } +static inline void PackGray565Store16(uint8x16_t y8, uint16_t* dst) { +#if defined(__aarch64__) + const uint16x8_t y_lo = vmovl_u8(vget_low_u8(y8)); + const uint16x8_t y_hi = vmovl_high_u8(y8); +#else + const uint16x8_t y_lo = vmovl_u8(vget_low_u8(y8)); + const uint16x8_t y_hi = vmovl_u8(vget_high_u8(y8)); +#endif + vst1q_u16(dst, PackGray565Neon(y_lo)); + vst1q_u16(dst + 8, PackGray565Neon(y_hi)); +} + +#if defined(__aarch64__) +// 256-byte LUT lookup via four 64-byte TBL windows (out-of-range lanes → 0). +static inline uint8x16_t Tbl256(uint8x16_t idx, const uint8x16x4_t& t0, + const uint8x16x4_t& t1, const uint8x16x4_t& t2, + const uint8x16x4_t& t3) { + const uint8x16_t r0 = vqtbl4q_u8(t0, idx); + const uint8x16_t r1 = vqtbl4q_u8(t1, vsubq_u8(idx, vdupq_n_u8(64))); + const uint8x16_t r2 = vqtbl4q_u8(t2, vsubq_u8(idx, vdupq_n_u8(128))); + const uint8x16_t r3 = vqtbl4q_u8(t3, vsubq_u8(idx, vdupq_n_u8(192))); + return vorrq_u8(vorrq_u8(r0, r1), vorrq_u8(r2, r3)); +} + +static void PackGray565RowNeonLimited(const uint8_t* src, uint16_t* dst, + uint32_t width) { + const uint8_t* expand = kLimitedToFullLut.data; + const uint8x16x4_t t0 = vld1q_u8_x4(expand + 0); + const uint8x16x4_t t1 = vld1q_u8_x4(expand + 64); + const uint8x16x4_t t2 = vld1q_u8_x4(expand + 128); + const uint8x16x4_t t3 = vld1q_u8_x4(expand + 192); + + uint32_t x = 0; + for (; x + 32 <= width; x += 32) { + __builtin_prefetch(src + x + 64); + const uint8x16_t y0 = Tbl256(vld1q_u8(src + x), t0, t1, t2, t3); + const uint8x16_t y1 = Tbl256(vld1q_u8(src + x + 16), t0, t1, t2, t3); + PackGray565Store16(y0, dst + x); + PackGray565Store16(y1, dst + x + 16); + } + for (; x + 16 <= width; x += 16) { + PackGray565Store16(Tbl256(vld1q_u8(src + x), t0, t1, t2, t3), dst + x); + } + for (; x + 8 <= width; x += 8) { + const uint8x8_t y8 = vget_low_u8(Tbl256(vcombine_u8(vld1_u8(src + x), + vdup_n_u8(0)), + t0, t1, t2, t3)); + vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(y8))); + } + const uint16_t* lut = kGray565LutLimited.data; + for (; x < width; ++x) { + dst[x] = lut[src[x]]; + } +} + +static void PackGray565RowNeonFull(const uint8_t* src, uint16_t* dst, + uint32_t width) { + uint32_t x = 0; + for (; x + 32 <= width; x += 32) { + __builtin_prefetch(src + x + 64); + PackGray565Store16(vld1q_u8(src + x), dst + x); + PackGray565Store16(vld1q_u8(src + x + 16), dst + x + 16); + } + for (; x + 16 <= width; x += 16) { + PackGray565Store16(vld1q_u8(src + x), dst + x); + } + for (; x + 8 <= width; x += 8) { + vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(vld1_u8(src + x)))); + } + const uint16_t* lut = kGray565LutFull.data; + for (; x < width; ++x) { + dst[x] = lut[src[x]]; + } +} +#else // !__aarch64__ (ARMv7 NEON) // Limited [16,235] → full [0,255]: e = ((y-16)*255+109)/219, with y clamped. // Uses (n * 1197) >> 18, exact for all 8-bit inputs after clamp. static uint16x8_t LimitedToFull8Neon(uint8x8_t y8) { y8 = vmax_u8(y8, vdup_n_u8(16)); y8 = vmin_u8(y8, vdup_n_u8(235)); const uint16x8_t t = vsubq_u16(vmovl_u8(y8), vdupq_n_u16(16)); - // n = t * 255 + 109 fits in uint16 (max 55984). const uint16x8_t n = vmlaq_n_u16(vdupq_n_u16(109), t, 255); - const uint32x4_t e_lo = vshrq_n_u32(vmulq_n_u32(vmovl_u16(vget_low_u16(n)), 1197), 18); + const uint32x4_t e_lo = + vshrq_n_u32(vmulq_n_u32(vmovl_u16(vget_low_u16(n)), 1197), 18); const uint32x4_t e_hi = vshrq_n_u32(vmulq_n_u32(vmovl_u16(vget_high_u16(n)), 1197), 18); return vcombine_u16(vmovn_u32(e_lo), vmovn_u32(e_hi)); } -static void PackGray565RowNeon(const uint8_t* src, uint16_t* dst, uint32_t width, - avifRange range) { - const bool limited = range == AVIF_RANGE_LIMITED; +static void PackGray565RowNeonLimited(const uint8_t* src, uint16_t* dst, + uint32_t width) { uint32_t x = 0; - if (limited) { - for (; x + 16 <= width; x += 16) { - const uint8x16_t y8 = vld1q_u8(src + x); - vst1q_u16(dst + x, PackGray565Neon(LimitedToFull8Neon(vget_low_u8(y8)))); - vst1q_u16(dst + x + 8, - PackGray565Neon(LimitedToFull8Neon(vget_high_u8(y8)))); - } - for (; x + 8 <= width; x += 8) { - vst1q_u16(dst + x, PackGray565Neon(LimitedToFull8Neon(vld1_u8(src + x)))); - } - } else { - for (; x + 16 <= width; x += 16) { - const uint8x16_t y8 = vld1q_u8(src + x); - vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(vget_low_u8(y8)))); - vst1q_u16(dst + x + 8, PackGray565Neon(vmovl_u8(vget_high_u8(y8)))); - } - for (; x + 8 <= width; x += 8) { - vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(vld1_u8(src + x)))); - } + for (; x + 16 <= width; x += 16) { + const uint8x16_t y8 = vld1q_u8(src + x); + vst1q_u16(dst + x, PackGray565Neon(LimitedToFull8Neon(vget_low_u8(y8)))); + vst1q_u16(dst + x + 8, + PackGray565Neon(LimitedToFull8Neon(vget_high_u8(y8)))); } - const uint16_t* lut = Gray565Lut(range); + for (; x + 8 <= width; x += 8) { + vst1q_u16(dst + x, PackGray565Neon(LimitedToFull8Neon(vld1_u8(src + x)))); + } + const uint16_t* lut = kGray565LutLimited.data; + for (; x < width; ++x) { + dst[x] = lut[src[x]]; + } +} + +static void PackGray565RowNeonFull(const uint8_t* src, uint16_t* dst, + uint32_t width) { + uint32_t x = 0; + for (; x + 16 <= width; x += 16) { + PackGray565Store16(vld1q_u8(src + x), dst + x); + } + for (; x + 8 <= width; x += 8) { + vst1q_u16(dst + x, PackGray565Neon(vmovl_u8(vld1_u8(src + x)))); + } + const uint16_t* lut = kGray565LutFull.data; for (; x < width; ++x) { dst[x] = lut[src[x]]; } } +#endif // __aarch64__ + +static void PackGray565RowNeon(const uint8_t* src, uint16_t* dst, uint32_t width, + avifRange range) { + if (range == AVIF_RANGE_LIMITED) { + PackGray565RowNeonLimited(src, dst, width); + } else { + PackGray565RowNeonFull(src, dst, width); + } +} #endif // __ARM_NEON static void PackGray565Row(const uint8_t* src, uint16_t* dst, uint32_t width, @@ -511,6 +611,11 @@ avifResult AvifImageToGray565Buffer(const HardwareBufferApi& hw_api, const uint8_t* src_row = src_y + row * src_row_bytes; uint16_t* dst_row = reinterpret_cast(dst_bytes + row * dst_row_bytes); +#if defined(__aarch64__) + if (row + 1 < height) { + __builtin_prefetch(src_y + (row + 1) * src_row_bytes); + } +#endif PackGray565Row(src_row, dst_row, width, range); }