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/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/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/README.md b/android_jni/README.md index 3f67047651..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) ``` @@ -39,6 +42,26 @@ $ ./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 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. + +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. + If you want to use libgav1 instead: ``` @@ -59,6 +82,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`. @@ -83,6 +109,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 @@ -105,3 +135,63 @@ 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 `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. +* `allowGray565` is `true`. + +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.) + +`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 +the RGB565 bit fields (R is the most-significant field): + +* 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. + +### 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 + +```java +if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + HardwareBuffer buffer = + AvifHardwareDecoder.decodeToHardwareBuffer( + encoded, encoded.remaining(), 0, 0, threads, /* allowGray565= */ true); + if (buffer != null) { + 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`, +`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/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/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/assets/README.md b/android_jni/avifandroidjni/src/androidTest/assets/README.md index f9463b98e8..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 @@ -22,3 +23,12 @@ 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, mono_8bpc_full.avif, mono_8bpc_ramp_full.avif + +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/AvifDecoderLengthValidationTest.java b/android_jni/avifandroidjni/src/androidTest/java/org/aomedia/avif/android/AvifDecoderLengthValidationTest.java index 38f623b1d3..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 @@ -274,15 +274,12 @@ public void isAvifImage_emptyDirectBuffer_returnsFalseNoCrash() { @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)); } @@ -295,8 +292,6 @@ public void create_emptyDirectBuffer_returnsNull() { @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); 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..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 @@ -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; @@ -20,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 { @@ -97,6 +106,12 @@ 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 && !image.isAnimated; + } + private static final Image[] IMAGES = { // Parameter ordering for still images: directory, filename, width, height, depth, alphaPresent, // threads. @@ -112,7 +127,8 @@ public ByteBuffer getBuffer() throws IOException { 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), @@ -124,12 +140,8 @@ public ByteBuffer getBuffer() throws IOException { 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}); - // 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; } @@ -158,6 +170,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 @@ -198,6 +215,11 @@ public void testDecodeRegularClass() throws IOException { 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); @@ -256,6 +278,83 @@ 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 = + 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); + 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 = + AvifHardwareDecoder.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(); + 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/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/AvifDecoder.java b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifDecoder.java index ee8c07e843..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 @@ -19,6 +19,9 @@ * 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 @@ -110,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); @@ -156,6 +157,11 @@ 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) { @@ -181,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 @@ -253,6 +257,10 @@ public int nthFrame(int n, Bitmap bitmap) { */ 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 new file mode 100644 index 0000000000..27f304f3f1 --- /dev/null +++ b/android_jni/avifandroidjni/src/main/java/org/aomedia/avif/android/AvifHardwareDecoder.java @@ -0,0 +1,193 @@ +// 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)}. + * + *

Scaling applies only when both {@code targetWidth} and {@code targetHeight} are positive. + * 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. + * @param length Length of the encoded buffer. + * @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 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 + public static HardwareBuffer decodeToHardwareBuffer( + ByteBuffer encoded, int length, int targetWidth, int targetHeight, int threads) { + return decodeToHardwareBuffer(encoded, length, targetWidth, targetHeight, threads, false); + } + + /** + * Decodes the AVIF image into an {@link HardwareBuffer}. + * + *

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}. + * + *

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: + * + *

    + *
  • Encoding: {@code Y = 4 * G6 + (R5 & 3)}, {@code B5 = 0} (R is MSB: {@code R<<11 | G<<5 | + * B}). + *
  • Display: callers must apply a restore {@link android.graphics.ColorMatrix} that + * maps each of R,G,B to {@code 31/255·r + 252/255·g}. Drawing without that matrix looks like + * greenish noise, not grayscale. + *
+ * + * @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 + public static HardwareBuffer decodeToHardwareBuffer( + ByteBuffer encoded, + int length, + int targetWidth, + int targetHeight, + int threads, + boolean allowGray565) { + return decodeToHardwareBufferNative( + encoded, length, targetWidth, targetHeight, threads, allowGray565); + } + + /** + * Decodes the AVIF image into an {@link HardwareBuffer} at the encoded 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, false); + } + + /** + * 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 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. + */ + @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 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 allowGray565) { + if (decoder == null || !decoder.isAlive()) { + return null; + } + return nextFrameHardwareBufferNative( + decoder.getNativeDecoderHandle(), targetWidth, targetHeight, allowGray565); + } + + /** Decodes the next frame at the encoded image dimensions. */ + @Nullable + public static HardwareBuffer nextFrameHardwareBuffer(AvifDecoder decoder) { + return nextFrameHardwareBuffer(decoder, 0, 0, false); + } + + /** + * 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 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. + * @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) { + return nthFrameHardwareBuffer(decoder, n, targetWidth, targetHeight, false); + } + + /** + * Decodes the nth frame of an animated AVIF into an {@link HardwareBuffer}. + * + * @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 allowGray565) { + if (decoder == null || !decoder.isAlive()) { + return null; + } + return nthFrameHardwareBufferNative( + decoder.getNativeDecoderHandle(), n, targetWidth, targetHeight, allowGray565); + } + + /** 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); + } + + private static native HardwareBuffer decodeToHardwareBufferNative( + ByteBuffer encoded, + int length, + int targetWidth, + int targetHeight, + int threads, + boolean allowGray565); + + private static native HardwareBuffer nextFrameHardwareBufferNative( + long nativeDecoderHandle, int targetWidth, int targetHeight, boolean allowGray565); + + private static native HardwareBuffer nthFrameHardwareBufferNative( + long nativeDecoderHandle, int n, int targetWidth, int targetHeight, boolean allowGray565); +} diff --git a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt index 186831772c..2607af5538 100644 --- a/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt +++ b/android_jni/avifandroidjni/src/main/jni/CMakeLists.txt @@ -29,6 +29,39 @@ 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 +# 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 @@ -37,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 cec1c880e5..80705fee3e 100644 --- a/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc +++ b/android_jni/avifandroidjni/src/main/jni/libavif_jni.cc @@ -1,16 +1,24 @@ // Copyright 2022 Google LLC // SPDX-License-Identifier: BSD-2-Clause +#include #include +#include #include -#include +#include #include +#include -#include #include +#include #include +#include #include +#if defined(__ARM_NEON) || defined(__ARM_NEON__) +#include +#endif + #include "avif/avif.h" #define LOG_TAG "avif_jni" @@ -25,12 +33,141 @@ 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 +#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* buffer_library = nullptr; + void* android_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 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() { + 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); +} + +void LoadHardwareBufferApi(HardwareBufferApi* api) { + if (GetDeviceApiLevel() < 29) { + LOGE("HardwareBuffer decode requires API 29+."); + return; + } + + // 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; + } + + 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; + } + + api->available = true; +} + +const HardwareBufferApi& GetHardwareBufferApi() { + static std::once_flag init_flag; + static HardwareBufferApi api; + std::call_once(init_flag, []() { LoadHardwareBufferApi(&api); }); + return api; +} + +int getThreadCount(int threads); +bool JniExceptionCheck(JNIEnv* env); + // RAII wrapper class that properly frees the decoder related objects on // destruction. struct AvifDecoderWrapper { @@ -47,7 +184,6 @@ struct AvifDecoderWrapper { } avifDecoder* decoder = nullptr; - avifCropRect crop; }; // Returns true when `encoded` is a direct ByteBuffer of at least `length` @@ -92,10 +228,15 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, decoder->decoder->maxThreads = threads; 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 - // 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 @@ -112,23 +253,389 @@ bool CreateDecoderAndParse(AvifDecoderWrapper* const decoder, LOGE("Failed to parse AVIF image: %s.", avifResultToString(res)); return false; } + return true; +} + +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->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& + scaling_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; + } + 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(); + avifDiagnostics diag; + *res = avifImageScale(image, dst_width, dst_height, &diag); + if (*res != AVIF_RESULT_OK) { + 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); + } + *res = AVIF_RESULT_OK; + 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; + 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, + int depth, avifBool is_float) { + avifResult res; + std::unique_ptr scaling_image( + nullptr, avifImageDestroy); + avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, + scaling_image); + if (image == nullptr) { + 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; + rgb_image.depth = depth; + rgb_image.isFloat = is_float; + rgb_image.pixels = static_cast(pixels); + rgb_image.rowBytes = row_bytes; + rgb_image.alphaPremultiplied = AVIF_TRUE; + res = avifImageYUVToRGB(image, &rgb_image); + if (res != AVIF_RESULT_OK) { + LOGE("Failed to convert YUV Pixels to RGB. Status: %d", res); + } + return res; +} + +// 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]; +}; + +struct LimitedToFullLutTable { + uint8_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 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 + : 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 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)); + 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 PackGray565RowNeonLimited(const uint8_t* src, uint16_t* dst, + uint32_t width) { + uint32_t x = 0; + 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)))); + } + 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, + 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) { + 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; + } + + 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); +#if defined(__aarch64__) + if (row + 1 < height) { + __builtin_prefetch(src_y + (row + 1) * src_row_bytes); + } +#endif + PackGray565Row(src_row, dst_row, width, range); + } + + hw_api.unlock(hw_buffer, nullptr); + return AVIF_RESULT_OK; +} - 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; +bool ShouldUseGray565Output(avifImage* image, bool allow_gray565) { + if (!allow_gray565) { + return false; + } + 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; + } + if (image->depth != 8) { + return false; } return true; } @@ -141,10 +648,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. - 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 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; } @@ -154,84 +660,181 @@ avifResult AvifImageToBitmap(JNIEnv* const env, LOGE("Failed to lock Bitmap."); return AVIF_RESULT_UNKNOWN_ERROR; } - avifImage* image; - std::unique_ptr cropped_image( - nullptr, avifImageDestroy); + + const avifResult res = AvifImageToRGBBuffer( + decoder, bitmap_pixels, bitmap_info.width, bitmap_info.height, + bitmap_info.stride, AVIF_RGB_FORMAT_RGBA, /*depth=*/8, /*is_float=*/AVIF_FALSE); + AndroidBitmap_unlockPixels(env, bitmap); + return res; +} + +jobject AvifImageToJavaHardwareBuffer(JNIEnv* const env, + AvifDecoderWrapper* const decoder, + uint32_t dst_width, uint32_t dst_height, + bool allow_gray565) { + 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; + } + 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) { - image = decoder->decoder->image; - } else { - cropped_image.reset(avifImageCreateEmpty()); - if (cropped_image == nullptr) { - LOGE("Failed to allocate cropped image."); - return AVIF_RESULT_OUT_OF_MEMORY; + std::unique_ptr scaling_image( + nullptr, avifImageDestroy); + avifImage* image = PrepareImageForOutput(decoder, dst_width, dst_height, &res, + scaling_image); + if (image == nullptr) { + return nullptr; + } + + AHardwareBuffer_Desc desc = {}; + desc.width = dst_width; + desc.height = dst_height; + desc.layers = 1; + desc.usage = AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY | + AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE; + + bool use_gray565 = ShouldUseGray565Output(image, allow_gray565); + + AHardwareBuffer* hw_buffer = nullptr; + if (use_gray565) { + desc.format = AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM; + if (hw_api.allocate(&desc, &hw_buffer) != 0) { + LOGE("AHardwareBuffer_allocate failed for R5G6B5; falling back to RGBA."); + use_gray565 = false; + hw_buffer = 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; + } + if (!use_gray565) { + desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; + if (hw_api.allocate(&desc, &hw_buffer) != 0) { + LOGE("AHardwareBuffer_allocate failed."); + 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->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 = 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; - } - image = image_copy.get(); + hw_api.describe(hw_buffer, &desc); + + avifResult fill_res; + if (use_gray565) { + // 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, + &pixels) != 0) { + LOGE("AHardwareBuffer_lock failed."); + hw_api.release(hw_buffer); + return nullptr; } - 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; + fill_res = AvifImageToRGBBufferFromImage( + image, pixels, static_cast(desc.stride) * 4); + hw_api.unlock(hw_buffer, nullptr); + } + if (fill_res != AVIF_RESULT_OK) { + hw_api.release(hw_buffer); + return nullptr; + } + + 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)) { + return nullptr; } } + return java_buffer; +} - 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; +jobject DecodeToHardwareBuffer(JNIEnv* const env, jobject encoded, int length, + int target_width, int target_height, + int threads, bool allow_gray565) { + const uint8_t* buffer = nullptr; + size_t size = 0; + if (!ValidateDirectBuffer(env, encoded, length, &buffer, &size)) { + return nullptr; } - rgb_image.pixels = static_cast(bitmap_pixels); - rgb_image.rowBytes = bitmap_info.stride; - // 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; + AvifDecoderWrapper decoder; + if (!CreateDecoderAndParse(&decoder, buffer, size, + getThreadCount(threads))) { + return nullptr; } - return AVIF_RESULT_OK; + uint32_t dst_width = 0; + 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; + } + return AvifImageToJavaHardwareBuffer(env, &decoder, dst_width, dst_height, + allow_gray565); +} + +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); + 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, + allow_gray565); +} + +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); + 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, + allow_gray565); } 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; @@ -241,7 +844,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; @@ -250,15 +864,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 @@ -326,9 +935,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); @@ -378,9 +987,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); @@ -444,6 +1053,40 @@ FUNC(jint, nthFrame, jlong jdecoder, jint n, jobject bitmap) { return DecodeNthImage(env, decoder, n, bitmap); } +HW_FUNC(jobject, decodeToHardwareBufferNative, jobject encoded, int length, + 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_gray565 != JNI_FALSE); +} + +HW_FUNC(jobject, nextFrameHardwareBufferNative, jlong jdecoder, jint target_width, + jint target_height, jboolean allow_gray565) { + IGNORE_UNUSED_HW_JNI_PARAMETERS; + AvifDecoderWrapper* const decoder = + reinterpret_cast(jdecoder); + if (decoder == nullptr) { + return nullptr; + } + return NextFrameToHardwareBuffer(env, decoder, target_width, target_height, + allow_gray565 != JNI_FALSE); +} + +HW_FUNC(jobject, nthFrameHardwareBufferNative, jlong jdecoder, jint n, + jint target_width, jint target_height, jboolean allow_gray565) { + 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, + allow_gray565 != JNI_FALSE); +} + FUNC(jstring, resultToString, jint result) { IGNORE_UNUSED_JNI_PARAMETERS; return env->NewStringUTF(avifResultToString(static_cast(result))); 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 b03bab8312..5dfe158385 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) @@ -27,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) @@ -74,6 +112,12 @@ function(avif_build_local_dav1d) endif() file(MAKE_DIRECTORY ${install_dir}/include) + 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( dav1d ${download_step_args} @@ -89,7 +133,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} ${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/check_dav1d_syms.sh b/ext/check_dav1d_syms.sh new file mode 100644 index 0000000000..1b8c3ec7ba --- /dev/null +++ b/ext/check_dav1d_syms.sh @@ -0,0 +1,17 @@ +#!/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 +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 2000a4ba98..769018bc97 100755 --- a/ext/dav1d_android.sh +++ b/ext/dav1d_android.sh @@ -1,37 +1,131 @@ #!/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. # # 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. +# +# 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 1.5.3 --depth 1 https://code.videolan.org/videolan/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" - -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]}" - 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 - PATH=$PATH:${android_bin} meson compile -C "dav1d/build/${abi}" -done +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 + + # -Db_lto=true so the static archive participates in the Android JNI LTO link. + 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 \ + -Denable_422_444=false \ + -Denable_frame_delay=false \ + -Denable_superres=false \ + -Denable_warp=false \ + -Denable_compound=false \ + "${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/libyuv_android.sh b/ext/libyuv_android.sh index d8cb38195f..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 @@ -28,10 +27,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 .. diff --git a/ext/rebuild_dav1d_arm64.sh b/ext/rebuild_dav1d_arm64.sh new file mode 100644 index 0000000000..d0198decec --- /dev/null +++ b/ext/rebuild_dav1d_arm64.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -ex +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 +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/include/avif/avif.h b/include/avif/avif.h index faab302089..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, @@ -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, @@ -1438,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/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/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..b7fbfbbb70 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) { @@ -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) @@ -6071,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); @@ -6097,10 +6122,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; } @@ -6230,7 +6255,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, @@ -6304,7 +6329,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, @@ -6465,13 +6490,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); diff --git a/src/reformat.c b/src/reformat.c index d1a511b87b..e5dcbb73cc 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); @@ -220,6 +225,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 +578,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. @@ -616,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 @@ -627,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; @@ -977,6 +994,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 +1325,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 +1437,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 +1507,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 +1565,7 @@ static avifResult avifImageYUVToRGBImpl(const avifImage * image, avifRGBImage * } } } +#endif } } @@ -1577,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. @@ -1648,6 +1661,14 @@ 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). + // 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; + } +#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) { @@ -1670,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 18329c80b6..8b12c35fe5 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 @@ -422,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 *, @@ -450,21 +464,7 @@ 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, const uint16_t *, @@ -517,27 +517,21 @@ typedef int (*YUVAToRGBMatrixHighBitDepth)(const uint16_t *, int, int, int); +#endif // 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; YUVToRGBMatrixHighBitDepth yuvToRgbMatrixHighBitDepth; YUVAToRGBMatrixHighBitDepth yuvaToRgbMatrixHighBitDepth; +#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. @@ -547,66 +541,31 @@ static avifBool getLibYUVConversionFunction(avifPixelFormat yuvFormat, avifBool alphaPreferred, LibyuvConversionFunction * lcf) { - // Lookup table for 8-bit YUV400 to 8-bit RGB Matrix. - static const YUV400ToRGBMatrix lutYuv400ToRgbMatrix[AVIF_RGB_FORMAT_COUNT] = { - // // AVIF_RGB_FORMAT_ - NULL, // RGB - I400ToARGBMatrix, // RGBA - NULL, // ARGB - NULL, // BGR - I400ToARGBMatrix, // BGRA - NULL, // ABGR - NULL, // RGB_565 - }; - - // Lookup table for 8-bit YUV To 8-bit RGB Matrix (with filter). + // 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, 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 - }; - - // 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 - }; - - // 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, 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 }; +#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,199 +669,54 @@ 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 (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. - } - 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 (yuvDepth != 8 || yuvFormat != AVIF_PIXEL_FORMAT_YUV420) { + 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; - 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; } } +#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 +742,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 +751,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 @@ -962,147 +777,37 @@ 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; - 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: 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], + image->yuvRowBytes[uPlaneIndex], + image->yuvPlanes[vPlaneIndex], + image->yuvRowBytes[vPlaneIndex], + rgb->pixels, + rgb->rowBytes, + matrix, + image->width, + image->height, + kFilterBilinear); + } 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, + kFilterBilinear); + *alphaReformattedWithLibYUV = AVIF_TRUE; } return (libyuvResult != 0) ? AVIF_RESULT_REFORMAT_FAILED : AVIF_RESULT_OK; } @@ -1137,45 +842,16 @@ 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; } 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) diff --git a/src/scale.c b/src/scale.c index 777eca48de..33db09da3d 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, @@ -42,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) { @@ -69,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; @@ -100,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 - } } } @@ -145,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: