Skip to content

Commit

Permalink
feat: support avif format output
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed Oct 15, 2021
1 parent a50bf55 commit f35b6ff
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 80 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
settings:
- host: macos-latest
target: 'x86_64-apple-darwin'
setup: brew install nasm
build: |
rustc --print target-cpus
pnpm build
Expand Down Expand Up @@ -85,6 +86,7 @@ jobs:
downloadTarget: 'aarch64-linux-android'
build: |
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
export PATH="${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin:${PATH}"
pnpm build -- --target aarch64-linux-android
name: stable - ${{ matrix.settings.target }} - node@14
Expand All @@ -106,6 +108,10 @@ jobs:
run: echo "C:\\msys64\\mingw64\\bin" >> $GITHUB_PATH
shell: bash

- name: Setup nasm
uses: ilammy/setup-nasm@v1
if: matrix.settings.host == 'windows-latest'

- name: Install
uses: actions-rs/toolchain@v1
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
profile: default
override: true

- name: Install nasm
uses: ilammy/setup-nasm@v1

- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/cargo-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ jobs:
npm install -g pnpm
pnpm install --frozen-lockfile
- name: Install nasm
run: brew install nasm

- name: Download skia binary
run: node ./scripts/release-skia-binary.js --download

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
node-version: 14
check-latest: true

- name: Install nasm
uses: ilammy/setup-nasm@v1

- name: Install
uses: actions-rs/toolchain@v1
with:
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ napi = {version = "1", features = ["serde-json"]}
napi-derive = "1"
nom = "7"
once_cell = "1"
ravif = "0.8"
regex = "1"
"rgb" = "0.8"
serde = "1"
serde_derive = "1"
serde_json = "1"
Expand Down
3 changes: 1 addition & 2 deletions __test__/canvas-class.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import test from 'ava'
import { omit } from 'lodash'

import { createCanvas, Canvas } from '../index'

test('Canvas constructor should be equal to createCanvas', (t) => {
t.deepEqual(omit(createCanvas(200, 100), 'getContext'), omit(new Canvas(200, 100), 'getContext'))
t.true(new Canvas(100, 100) instanceof createCanvas(100, 100).constructor)
})
6 changes: 6 additions & 0 deletions __test__/draw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,12 @@ test('webp-output', async (t) => {
await snapshotImage(t, t.context, 'webp')
})

test('avif-output', async (t) => {
const { ctx } = t.context
drawTranslate(ctx)
await snapshotImage(t, t.context, 'avif')
})

test('raw output', async (t) => {
const { ctx, canvas } = t.context
drawTranslate(ctx)
Expand Down
2 changes: 1 addition & 1 deletion __test__/image-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const ARCH_NAME = arch()
export async function snapshotImage<C>(
t: ExecutionContext<C>,
context = t.context,
type: 'png' | 'jpeg' | 'webp' = 'png',
type: 'png' | 'jpeg' | 'webp' | 'avif' = 'png',
differentRatio = ARCH_NAME === 'x64' ? 0.015 : t.title.indexOf('filter') > -1 ? 2.5 : 0.3,
) {
// @ts-expect-error
Expand Down
Binary file added __test__/snapshots/avif-output.avif
Binary file not shown.
15 changes: 14 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ export interface SvgCanvas {
getContent(): Buffer
}

export interface AvifConfig {
quality?: number
alphaQuality?: number
speed?: number
threads?: number
}

export class Canvas {
constructor(width: number, height: number, flag?: SvgExportFlag)

Expand All @@ -304,19 +311,25 @@ export class Canvas {
getContext(contextType: '2d', contextAttributes?: ContextAttributes): SKRSContext2D
encodeSync(format: 'webp' | 'jpeg', quality?: number): Buffer
encodeSync(format: 'png'): Buffer
encodeSync(format: 'avif', cfg?: AvifConfig): Buffer
encode(format: 'webp' | 'jpeg', quality?: number): Promise<Buffer>
encode(format: 'png'): Promise<Buffer>
encode(format: 'avif', cfg?: AvifConfig): Promise<Buffer>

toBuffer(mime: 'image/png' | 'image/jpeg' | 'image/webp'): Buffer
toBuffer(mime: 'image/png'): Buffer
toBuffer(mime: 'image/jpeg' | 'image/webp', quality?: number): Buffer
toBuffer(mime: 'image/avif', cfg?: AvifConfig): Buffer
// raw pixels
data(): Buffer
toDataURL(mime?: 'image/png'): string
toDataURL(mime: 'image/jpeg' | 'image/webp', quality?: number): string
toDataURL(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): string
toDataURL(mime?: 'image/avif', cfg?: AvifConfig): string

toDataURLAsync(mime?: 'image/png'): Promise<string>
toDataURLAsync(mime: 'image/jpeg' | 'image/webp', quality?: number): Promise<string>
toDataURLAsync(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): Promise<string>
toDataURLAsync(mime?: 'image/avif', cfg?: AvifConfig): Promise<string>
}

export function createCanvas(width: number, height: number): Canvas
Expand Down
93 changes: 93 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,99 @@ function createCanvas(width, height, flag) {
return ctx
}

const {
encode: canvasEncode,
encodeSync: canvasEncodeSync,
toBuffer: canvasToBuffer,
toDataURL: canvasToDataURL,
toDataURLAsync: canvasToDataURLAsync,
} = Object.getPrototypeOf(canvasElement)

canvasElement.encode = function encode(type, qualityOrConfig) {
if (type === 'avif') {
return canvasEncode.call(
this,
type,
JSON.stringify({
quality: 92,
alphaQuality: 92,
threads: 0,
speed: 1,
...(qualityOrConfig || {}),
}),
)
}
return canvasEncode.call(this, type, qualityOrConfig || 92)
}

canvasElement.encodeSync = function encodeSync(type, qualityOrConfig) {
if (type === 'avif') {
return canvasEncodeSync.call(
this,
type,
JSON.stringify({
quality: 92,
alphaQuality: 92,
threads: 0,
speed: 1,
...(qualityOrConfig || {}),
}),
)
}
return canvasEncodeSync.call(this, type, qualityOrConfig || 92)
}

canvasElement.toBuffer = function toBuffer(type = 'image/png', qualityOrConfig) {
if (type === 'avif') {
return canvasToBuffer.call(
this,
type,
JSON.stringify({
quality: 92,
alphaQuality: 92,
threads: 0,
speed: 1,
...(qualityOrConfig || {}),
}),
)
}
return canvasToBuffer.call(this, type, qualityOrConfig || 92)
}

canvasElement.toDataURL = function toDataURL(type = 'image/png', qualityOrConfig) {
if (type === 'avif') {
return canvasToDataURL.call(
this,
type,
JSON.stringify({
quality: 92,
alphaQuality: 92,
threads: 0,
speed: 1,
...(qualityOrConfig || {}),
}),
)
}
return canvasToDataURL.call(this, type, qualityOrConfig || 92)
}

canvasElement.toDataURLAsync = function toDataURLAsync(type = 'image/png', qualityOrConfig) {
if (type === 'avif') {
return canvasToDataURLAsync.call(
this,
type,
JSON.stringify({
quality: 92,
alphaQuality: 92,
threads: 0,
speed: 1,
...(qualityOrConfig || {}),
}),
)
}
return canvasToDataURLAsync.call(this, type, qualityOrConfig || 92)
}

return canvasElement
}

Expand Down
1 change: 1 addition & 0 deletions musl.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ RUN apk add --update --no-cache musl-dev wget && \
git \
build-base \
clang \
nasm \
llvm \
nasm \
gn \
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"ava": {
"require": ["@swc-node/register"],
"extensions": ["ts"],
"timeout": "30s",
"timeout": "3m",
"environmentVariables": {
"SWC_NODE_PROJECT": "./tsconfig.json",
"NODE_ENV": "ava"
Expand Down
85 changes: 68 additions & 17 deletions src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ use std::f32::consts::PI;
use std::mem;
use std::rc::Rc;
use std::result;
use std::slice;
use std::str::FromStr;

use cssparser::{Color as CSSColor, Parser, ParserInput};
use napi::*;
use rgb::FromSlice;

use crate::filter::css_filters_to_image_filter;
use crate::{
Expand Down Expand Up @@ -2128,29 +2130,50 @@ fn get_text_baseline(ctx: CallContext) -> Result<JsString> {
.create_string(context_2d.state.text_baseline.as_str())
}

#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
pub struct AVIFConfig {
pub quality: f32,
#[serde(rename = "alphaQuality")]
pub alpha_quality: f32,
pub speed: u8,
pub threads: u8,
}

pub enum ContextData {
Png(SurfaceRef),
Jpeg(SurfaceRef, u8),
Webp(SurfaceRef, u8),
Avif(SurfaceRef, AVIFConfig, u32, u32),
}

unsafe impl Send for ContextData {}
unsafe impl Sync for ContextData {}

pub enum ContextOutputData {
Skia(SkiaDataRef),
Avif(Vec<u8>),
}

impl Task for ContextData {
type Output = SkiaDataRef;
type Output = ContextOutputData;
type JsValue = JsBuffer;

fn compute(&mut self) -> Result<Self::Output> {
match self {
ContextData::Png(surface) => surface.png_data().ok_or_else(|| {
Error::new(
Status::GenericFailure,
"Get png data from surface failed".to_string(),
)
}),
ContextData::Png(surface) => {
surface
.png_data()
.map(ContextOutputData::Skia)
.ok_or_else(|| {
Error::new(
Status::GenericFailure,
"Get png data from surface failed".to_string(),
)
})
}
ContextData::Jpeg(surface, quality) => surface
.encode_data(SkEncodedImageFormat::Jpeg, *quality)
.map(ContextOutputData::Skia)
.ok_or_else(|| {
Error::new(
Status::GenericFailure,
Expand All @@ -2159,25 +2182,53 @@ impl Task for ContextData {
}),
ContextData::Webp(surface, quality) => surface
.encode_data(SkEncodedImageFormat::Webp, *quality)
.map(ContextOutputData::Skia)
.ok_or_else(|| {
Error::new(
Status::GenericFailure,
"Get webp data from surface failed".to_string(),
)
}),
ContextData::Avif(surface, config, width, height) => surface
.data()
.ok_or_else(|| {
Error::new(
Status::GenericFailure,
"Get avif data from surface failed".to_string(),
)
})
.and_then(|(data, size)| {
ravif::encode_rgba(
ravif::Img::new(
unsafe { slice::from_raw_parts(data, size) }.as_rgba(),
*width as usize,
*height as usize,
),
&ravif::Config {
quality: config.quality,
alpha_quality: config.alpha_quality,
speed: config.speed,
premultiplied_alpha: false,
threads: 0,
color_space: ravif::ColorSpace::RGB,
},
)
.map(|(o, _width, _height)| ContextOutputData::Avif(o))
.map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))
}),
}
}

fn resolve(self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
unsafe {
env
.create_buffer_with_borrowed_data(
output.0.ptr,
output.0.size,
output,
|data_ref: Self::Output, _| mem::drop(data_ref),
)
.map(|value| value.into_raw())
fn resolve(self, env: Env, output_data: Self::Output) -> Result<Self::JsValue> {
match output_data {
ContextOutputData::Skia(output) => unsafe {
env
.create_buffer_with_borrowed_data(output.0.ptr, output.0.size, output, |data_ref, _| {
mem::drop(data_ref)
})
.map(|value| value.into_raw())
},
ContextOutputData::Avif(output) => env.create_buffer_with_data(output).map(|b| b.into_raw()),
}
}
}
Loading

1 comment on commit f35b6ff

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: f35b6ff Previous: a50bf55 Ratio
Draw house#skia-canvas 27 ops/sec (±0.13%) 27 ops/sec (±0.37%) 1
Draw house#node-canvas 21 ops/sec (±0.32%) 21 ops/sec (±0.35%) 1
Draw house#@napi-rs/skia 26 ops/sec (±0.08%) 25 ops/sec (±0.19%) 0.96
Draw gradient#skia-canvas 26 ops/sec (±0.21%) 26 ops/sec (±0.31%) 1
Draw gradient#node-canvas 20 ops/sec (±0.21%) 20 ops/sec (±0.2%) 1
Draw gradient#@napi-rs/skia 25 ops/sec (±0.13%) 24 ops/sec (±0.09%) 0.96

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.