Skip to content

Commit

Permalink
fix: decode avif Image with libavif (#1005)
Browse files Browse the repository at this point in the history
* fix: enable libavif in skia build

* Fix avif decode

* Workaround

* Clippy [skip skia]

* Clippy [skip skia]

* Drop [skip skia]

* Update toolchain[skip skia]

* fmt [skip skia]

* avif image lifetime [skip skia]
  • Loading branch information
Brooooooklyn authored Mar 4, 2025
1 parent 7788740 commit d369c55
Show file tree
Hide file tree
Showing 16 changed files with 243 additions and 35 deletions.
8 changes: 6 additions & 2 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ jobs:
target: 'x86_64-unknown-linux-gnu'
downloadTarget: ''
docker: ghcr.io/brooooooklyn/canvas/ubuntu-builder:jammy
build: yarn build --target x86_64-unknown-linux-gnu
build: >-
rustup install &&
rustup target add x86_64-unknown-linux-gnu &&
yarn build --target x86_64-unknown-linux-gnu
- host: ubuntu-latest
downloadTarget: 'x86_64-unknown-linux-musl'
target: 'x86_64-unknown-linux-musl'
Expand All @@ -112,6 +115,7 @@ jobs:
docker: ghcr.io/brooooooklyn/canvas/ubuntu-builder:jammy-aarch64
build: >-
set -e &&
rustup install &&
rustup target add aarch64-unknown-linux-gnu &&
yarn build --target aarch64-unknown-linux-gnu
- host: ubuntu-24.04-arm
Expand Down Expand Up @@ -167,7 +171,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
cache: 'yarn'

- name: Set env
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/skia.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,23 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
choco install llvm ninja -y
choco upgrade llvm
choco upgrade llvm --version "19.1.7"
pip install certifi
- name: Apply workaround for https://github.com/llvm/llvm-project/issues/95133
if: runner.os == 'Windows'
shell: cmd
run: |
perl -i -ne "print unless /unsigned __int32 xbegin\(void\);/" "C:\Program Files\LLVM\lib\clang\19\include\intrin.h"
perl -i -ne "print unless /void _xend\(void\);/" "C:\Program Files\LLVM\lib\clang\19\include\intrin.h"
findstr /C:"unsigned __int32 xbegin(void);" "C:\Program Files\LLVM\lib\clang\19\include\intrin.h" || exit /b 0
findstr /C:"void _xend(void);" "C:\Program Files\LLVM\lib\clang\19\include\intrin.h" || exit /b 0
# See https://github.com/ilammy/msvc-dev-cmd?tab=readme-ov-file#caveats
- name: Remove GNU link.exe from GH actions
if: runner.os == 'Windows'
run: rm /usr/bin/link
shell: bash

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
if: matrix.os == 'ubuntu-latest'
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ base64-simd = "0.8"
cssparser = "0.29"
infer = "0.19"
libavif = { version = "0.14", default-features = false, features = ["codec-aom"] }
libavif-sys = { version = "0.17", default-features = false, features = ["codec-aom"] }
napi = { version = "3.0.0-alpha.27", default-features = false, features = ["napi4", "web_stream", "serde-json"] }
napi-derive = { version = "3.0.0-alpha.25", default-features = false }
nom = "8"
Expand Down
2 changes: 1 addition & 1 deletion README-zh.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `skr canvas`

![CI](https://github.com/Brooooooklyn/canvas/workflows/CI/badge.svg)
![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm133-hotpink)
![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm134-hotpink)
[![install size](https://packagephobia.com/badge?p=@napi-rs/canvas)](https://packagephobia.com/result?p=@napi-rs/canvas)
[![Downloads](https://img.shields.io/npm/dm/@napi-rs/canvas.svg?sanitize=true)](https://npmcharts.com/compare/@napi-rs/canvas?minimal=true)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `skr canvas`

[![CI](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml/badge.svg)](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml)
![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm133-hotpink)
![Skia Version](https://img.shields.io/badge/Skia-chrome%2Fm134-hotpink)
[![install size](https://packagephobia.com/badge?p=@napi-rs/canvas)](https://packagephobia.com/result?p=@napi-rs/canvas)
[![Downloads](https://img.shields.io/npm/dm/@napi-rs/canvas.svg?sanitize=true)](https://npmcharts.com/compare/@napi-rs/canvas?minimal=true)

Expand Down
Binary file added __test__/fixtures/issue-996.avif
Binary file not shown.
9 changes: 9 additions & 0 deletions __test__/regression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,12 @@ test('transform-with-non-inverted-matrix', (t) => {
ctx.transform(0, 0, 0, 0, 1019, 1165)
})
})

// https://github.com/Brooooooklyn/canvas/issues/996
test('draw-avif-image', async (t) => {
const canvas = createCanvas(1920, 1080)
const ctx = canvas.getContext('2d')
const image = await loadImage(join(__dirname, 'fixtures', 'issue-996.avif'))
ctx.drawImage(image, 0, 0)
await snapshotImage(t, { ctx, canvas })
})
Binary file added __test__/snapshots/draw-avif-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.84.0"
channel = "1.85.0"
profile = "default"
17 changes: 8 additions & 9 deletions scripts/build-skia.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ const GN_ARGS = [
`skia_use_harfbuzz=true`,
`skia_use_icu=true`,
`skia_use_libheif=true`,
// the libavif would conflict with the Rust libavif, use the Rust library to handle avif images
`skia_use_libavif=false`,
`skia_use_libjxl_decode=${!TARGET_TRIPLE.startsWith('riscv64')}`,
`skia_use_libjpeg_turbo_decode=true`,
`skia_use_libjpeg_turbo_encode=true`,
`skia_use_libwebp_decode=true`,
Expand All @@ -83,7 +86,7 @@ const GN_ARGS = [
`skia_enable_fontmgr_custom_empty=true`,
`skia_enable_fontmgr_android=false`,
`skunicode_tests_enabled=false`,
`skia_enable_skshaper_tests=false`
`skia_enable_skshaper_tests=false`,
]

switch (PLATFORM_NAME) {
Expand Down Expand Up @@ -128,11 +131,7 @@ switch (PLATFORM_NAME) {
'"-DSK_CODEC_DECODES_JPEG",' +
'"-DSK_HAS_HEIF_LIBRARY",' +
'"-DSK_SHAPER_HARFBUZZ_AVAILABLE"'
if (
PLATFORM_NAME === 'linux' &&
!TARGET_TRIPLE &&
HOST_ARCH === 'x64'
) {
if (PLATFORM_NAME === 'linux' && !TARGET_TRIPLE && HOST_ARCH === 'x64') {
if (HOST_LIBC === 'glibc') {
ExtraCflagsCC += ',"-stdlib=libc++","-static","-I/usr/lib/llvm-18/include/c++/v1"'
} else {
Expand Down Expand Up @@ -181,8 +180,8 @@ switch (TARGET_TRIPLE) {
)
break
case 'armv7-unknown-linux-gnueabihf':
CC='"arm-linux-gnueabihf-gcc"'
CXX='"arm-linux-gnueabihf-g++"'
CC = '"arm-linux-gnueabihf-gcc"'
CXX = '"arm-linux-gnueabihf-g++"'
ExtraSkiaBuildFlag += ' target_cpu="armv7a" target_os="linux"'
ExtraCflags = `"-march=armv7-a", "-mthumb", "-mfpu=neon"`
break
Expand Down Expand Up @@ -226,7 +225,7 @@ switch (TARGET_TRIPLE) {
ExtraSkiaBuildFlag += ' target_cpu="riscv64" target_os="linux"'
CC = '"riscv64-linux-gnu-gcc"'
CXX = '"riscv64-linux-gnu-g++"'
break;
break
case '':
break
default:
Expand Down
2 changes: 1 addition & 1 deletion skia
Submodule skia updated 373 files
154 changes: 154 additions & 0 deletions src/avif.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::result;

use libavif::{AvifData, RgbPixels, YuvFormat};
use libavif_sys as sys;
use napi::bindgen_prelude::*;
use napi_derive::napi;

Expand Down Expand Up @@ -134,3 +135,156 @@ pub(crate) fn encode(
encoder.set_max_threads(config.threads);
encoder.encode(&image).map_err(SkError::EncodeAvifError)
}

/// Enum representing AVIF error codes
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AvifErrorCode {
Ok = 0,
UnknownError = 1,
InvalidFtyp = 2,
NoContent = 3,
NoYuvFormatSelected = 4,
ReformatFailed = 5,
UnsupportedDepth = 6,
EncodeColorFailed = 7,
EncodeAlphaFailed = 8,
BmffParseFailed = 9,
MissingImageItem = 10,
DecodeColorFailed = 11,
DecodeAlphaFailed = 12,
ColorAlphaSizeMismatch = 13,
IspeSizeMismatch = 14,
NoCodecAvailable = 15,
NoImagesRemaining = 16,
InvalidExifPayload = 17,
InvalidImageGrid = 18,
InvalidCodecSpecificOption = 19,
TruncatedData = 20,
IoNotSet = 21,
IoError = 22,
WaitingOnIo = 23,
InvalidArgument = 24,
NotImplemented = 25,
OutOfMemory = 26,
CannotChangeSetting = 27,
IncompatibleImage = 28,
InternalError = 29,
EncodeGainMapFailed = 30,
DecodeGainMapFailed = 31,
InvalidToneMappedImage = 32,
}

#[derive(Debug)]
pub enum AvifError {
Known(AvifErrorCode),
Unknown(u32),
}

impl std::fmt::Display for AvifError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AvifError::Known(code) => write!(f, "AvifError: {:?}", code),
AvifError::Unknown(code) => write!(f, "AvifError: {}", code),
}
}
}

impl std::error::Error for AvifError {}

impl From<u32> for AvifError {
fn from(code: u32) -> Self {
match code {
0 => AvifError::Known(AvifErrorCode::Ok),
1 => AvifError::Known(AvifErrorCode::UnknownError),
2 => AvifError::Known(AvifErrorCode::InvalidFtyp),
3 => AvifError::Known(AvifErrorCode::NoContent),
4 => AvifError::Known(AvifErrorCode::NoYuvFormatSelected),
5 => AvifError::Known(AvifErrorCode::ReformatFailed),
6 => AvifError::Known(AvifErrorCode::UnsupportedDepth),
7 => AvifError::Known(AvifErrorCode::EncodeColorFailed),
8 => AvifError::Known(AvifErrorCode::EncodeAlphaFailed),
9 => AvifError::Known(AvifErrorCode::BmffParseFailed),
10 => AvifError::Known(AvifErrorCode::MissingImageItem),
11 => AvifError::Known(AvifErrorCode::DecodeColorFailed),
12 => AvifError::Known(AvifErrorCode::DecodeAlphaFailed),
13 => AvifError::Known(AvifErrorCode::ColorAlphaSizeMismatch),
14 => AvifError::Known(AvifErrorCode::IspeSizeMismatch),
15 => AvifError::Known(AvifErrorCode::NoCodecAvailable),
16 => AvifError::Known(AvifErrorCode::NoImagesRemaining),
17 => AvifError::Known(AvifErrorCode::InvalidExifPayload),
18 => AvifError::Known(AvifErrorCode::InvalidImageGrid),
19 => AvifError::Known(AvifErrorCode::InvalidCodecSpecificOption),
20 => AvifError::Known(AvifErrorCode::TruncatedData),
21 => AvifError::Known(AvifErrorCode::IoNotSet),
22 => AvifError::Known(AvifErrorCode::IoError),
23 => AvifError::Known(AvifErrorCode::WaitingOnIo),
24 => AvifError::Known(AvifErrorCode::InvalidArgument),
25 => AvifError::Known(AvifErrorCode::NotImplemented),
26 => AvifError::Known(AvifErrorCode::OutOfMemory),
27 => AvifError::Known(AvifErrorCode::CannotChangeSetting),
28 => AvifError::Known(AvifErrorCode::IncompatibleImage),
29 => AvifError::Known(AvifErrorCode::InternalError),
30 => AvifError::Known(AvifErrorCode::EncodeGainMapFailed),
31 => AvifError::Known(AvifErrorCode::DecodeGainMapFailed),
32 => AvifError::Known(AvifErrorCode::InvalidToneMappedImage),
_ => AvifError::Unknown(code),
}
}
}

impl AvifError {
pub fn from_code(code: u32) -> result::Result<(), AvifError> {
match code {
0 => Ok(()),
_ => Err(AvifError::Unknown(code)),
}
}
}

pub struct AvifImage {
image: *mut sys::avifImage,
rgb_image: sys::avifRGBImage,
pub width: u32,
pub height: u32,
pub row_bytes: u32,
pub data: *mut u8,
}

impl AvifImage {
pub fn decode_from(avif_bytes: &[u8]) -> result::Result<Self, AvifError> {
let decoder = unsafe { sys::avifDecoderCreate() };
let image = unsafe { sys::avifImageCreateEmpty() };
AvifError::from_code(unsafe {
sys::avifDecoderReadMemory(decoder, image, avif_bytes.as_ptr(), avif_bytes.len())
})?;

unsafe {
sys::avifDecoderDestroy(decoder);
}
let mut rgb_image = sys::avifRGBImage::default();
unsafe {
sys::avifRGBImageSetDefaults(&mut rgb_image, image);
rgb_image.format = sys::AVIF_RGB_FORMAT_RGBA;
rgb_image.depth = 8;
AvifError::from_code(sys::avifRGBImageAllocatePixels(&mut rgb_image))?;
AvifError::from_code(sys::avifImageYUVToRGB(image, &mut rgb_image))?;
};
Ok(Self {
image,
width: unsafe { (*image).width },
height: unsafe { (*image).height },
data: rgb_image.pixels,
row_bytes: rgb_image.rowBytes,
rgb_image,
})
}
}

impl Drop for AvifImage {
fn drop(&mut self) {
unsafe {
sys::avifRGBImageFreePixels(&mut self.rgb_image);
sys::avifImageDestroy(self.image);
}
}
}
4 changes: 2 additions & 2 deletions src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ impl Context {
state.shadow_offset_y,
sigma_x,
sigma_y,
(a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | b as u32,
((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32,
None,
)?;
drop_shadow_paint.set_alpha(shadow_alpha);
Expand Down Expand Up @@ -687,7 +687,7 @@ impl Context {
0.0,
sigma_x,
sigma_y,
(a as u32) << 24 | (r as u32) << 16 | (g as u32) << 8 | b as u32,
((a as u32) << 24) | ((r as u32) << 16) | ((g as u32) << 8) | b as u32,
None,
)?;
drop_shadow_paint.set_alpha(shadow_alpha);
Expand Down
6 changes: 3 additions & 3 deletions src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,9 @@ pub(crate) fn css_filters_to_image_filter(filters: Vec<CssFilter>) -> Option<Ima
offset_y,
sigma,
sigma,
(shadow_color.alpha as u32) << 24
| (shadow_color.red as u32) << 16
| (shadow_color.green as u32) << 8
((shadow_color.alpha as u32) << 24)
| ((shadow_color.red as u32) << 16)
| ((shadow_color.green as u32) << 8)
| shadow_color.blue as u32,
Some(&image_filter),
)
Expand Down
Loading

0 comments on commit d369c55

Please sign in to comment.