Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5aa8489
Hardware Buffer対応
k2w4t4h Jul 11, 2026
b22a1bf
過去Verを考慮する
k2w4t4h Jul 11, 2026
a7cf17e
Animation Avifの対応は不要なので消す
k2w4t4h Jul 11, 2026
532ff8a
dlの取得元対応
k2w4t4h Jul 11, 2026
ef78ba8
レビューの指摘に対応
k2w4t4h Jul 11, 2026
824ed9c
Fork差分が小さくなるように
k2w4t4h Jul 11, 2026
dbee1dd
パスが解決できなかったので書き方変える
k2w4t4h Jul 12, 2026
63e4f0b
AHardwareBuffer_toHardwareBuffer の読み込み先の調整
k2w4t4h Jul 13, 2026
bb619ee
R8対応とりあえず
k2w4t4h Jul 13, 2026
2ee82f1
BugFix
k2w4t4h Jul 13, 2026
93a8b38
RGB565に切替
k2w4t4h Jul 14, 2026
c712826
Merge pull request #2 from link-u/feature/hardware_buffer_r8
k2w4t4h Jul 15, 2026
4974385
Performance
k2w4t4h Jul 15, 2026
a7fe677
dav1dのVerあげる
k2w4t4h Jul 15, 2026
3227f53
無駄コピーを減らす
k2w4t4h Jul 15, 2026
b9e7361
リサイズのフィルタを速いやつに
k2w4t4h Jul 15, 2026
efc42b3
Neon SIMD
k2w4t4h Jul 15, 2026
34540e0
ビルド変更
k2w4t4h Jul 17, 2026
4ce90c6
色々削る
k2w4t4h Jul 18, 2026
07b949f
Alpha の不要デコードを省く
k2w4t4h Jul 18, 2026
ac65468
不要なメモリの開放
k2w4t4h Jul 18, 2026
a4ede21
RGB565の真の方を消す
k2w4t4h Jul 18, 2026
c38b965
NDK27対応
k2w4t4h Jul 18, 2026
970bd6b
不要なAPIの絞り込み
k2w4t4h Jul 18, 2026
37d6b9e
BugFix
k2w4t4h Jul 19, 2026
2cddfd5
BugFix
k2w4t4h Jul 19, 2026
72d37ae
android_jni: GL ES R8 HardwareBuffer output for 8-bit monochrome AVIF
k2w4t4h Sep 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci-android-emulator-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
4 changes: 2 additions & 2 deletions .github/workflows/ci-android-jni.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 15 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 "")

Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down
135 changes: 134 additions & 1 deletion android_jni/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

```
Expand All @@ -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:

```
Expand All @@ -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`.
Expand All @@ -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
Expand All @@ -105,3 +135,106 @@ 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.

### R8 (GL ES) output

Pass `allowR8 = true` (the trailing parameter of the 7-argument `decodeToHardwareBuffer` and of
the 5/6-argument `nextFrameHardwareBuffer` / `nthFrameHardwareBuffer` overloads) to opt in to a
**1 byte/pixel** `HardwareBuffer.R_8` (`0x38`) output. It is selected when **all** of the
following hold:

* The decoded image is 8-bit monochrome (`YUV400`) with no alpha plane (same as Gray565).
* `allowR8` is `true`.
* The device runs API 33+ (`Bitmap.wrapHardwareBuffer` only accepts `R_8` from Android 13).
* An offscreen OpenGL ES 3.0 context with `EGL_ANDROID_image_native_buffer`,
`EGL_ANDROID_get_native_client_buffer` and `GL_OES_EGL_image` can be created.

Why GL ES: CPU writes (`AHardwareBuffer_lock`) into `R8` buffers are gralloc-dependent and fail or
mis-stride on several low-end devices, which is why Gray565 exists. `R8` is, however, a mandatory
color-renderable format in GLES 3.0, so the JNI layer allocates the buffer with
`GPU_COLOR_OUTPUT | GPU_SAMPLED_IMAGE`, imports it as an `EGLImage`, and renders the Y plane into
it with a trivial fragment shader (`gles_r8_writer.cc`). Limited-range Y (16–235) is expanded to
0–255 in the shader; full-range Y is copied as-is. The call blocks (`glFinish`) until the GPU has
finished, so the buffer is safe to wrap immediately.

Format priority is **R8 → Gray565 → RGBA_8888**. Every R8 failure (allocation, EGL/GL setup,
missing extensions, incomplete framebuffer) is logged and falls through to the next candidate, so
callers must always dispatch on `HardwareBuffer.getFormat()`.

**Display responsibility:** Skia samples an `R_8` hardware bitmap as red-only. Draw it with a
`ColorMatrixColorFilter` that copies R into R, G and B:

```java
new ColorMatrix(new float[] {
1, 0, 0, 0, 0,
1, 0, 0, 0, 0,
1, 0, 0, 0, 0,
0, 0, 0, 1, 0});
```

The shared library now links against `EGL` and `GLESv3` (NDK system libraries). A process-wide
offscreen EGL context is created lazily on first use and kept alive; concurrent decodes serialize
on it. The caller's own EGL context (if any) is saved and restored around each write.

### 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, /* allowR8= */ true);
if (buffer != null) {
if (buffer.getFormat() == 0x38 /* HardwareBuffer.R_8, API 33 */) {
// Apply the R8 restore ColorMatrix (copy R to R, G, B) before drawing.
} else 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).

2 changes: 1 addition & 1 deletion android_jni/avifandroidjni/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ plugins {
android {
namespace 'org.aomedia.avif.android'
compileSdk 31
ndkVersion "25.2.9519653"
ndkVersion "27.2.12479018"

defaultConfig {
minSdk 21
Expand Down
3 changes: 3 additions & 0 deletions android_jni/avifandroidjni/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@
-keep class org.aomedia.avif.android.AvifDecoder$Info {
*;
}
-keep class org.aomedia.avif.android.AvifHardwareDecoder {
*;
}
14 changes: 12 additions & 2 deletions android_jni/avifandroidjni/src/androidTest/assets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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).
88 changes: 88 additions & 0 deletions android_jni/avifandroidjni/src/androidTest/assets/gen_mono_ramp.c
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char** argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <limited|full> <out.avif>\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;
}
Loading
Loading