From f35b6ffc28b40147b97244526f25173292453db8 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Sun, 3 Oct 2021 01:31:26 +0800 Subject: [PATCH] feat: support avif format output --- .github/workflows/CI.yaml | 6 + .github/workflows/bench.yml | 3 + .github/workflows/cargo-test.yaml | 3 + .github/workflows/lint.yml | 3 + Cargo.toml | 2 + __test__/canvas-class.spec.ts | 3 +- __test__/draw.spec.ts | 6 + __test__/image-snapshot.ts | 2 +- __test__/snapshots/avif-output.avif | Bin 0 -> 3587 bytes index.d.ts | 15 +- index.js | 93 +++++++++++++ musl.Dockerfile | 1 + package.json | 2 +- src/ctx.rs | 85 +++++++++--- src/lib.rs | 207 ++++++++++++++++++++-------- 15 files changed, 351 insertions(+), 80 deletions(-) create mode 100644 __test__/snapshots/avif-output.avif diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 45527c37..8ebc5558 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -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 @@ -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 @@ -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: diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index dc716f3d..f35d1f79 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -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: diff --git a/.github/workflows/cargo-test.yaml b/.github/workflows/cargo-test.yaml index 316ddb4a..b078271a 100644 --- a/.github/workflows/cargo-test.yaml +++ b/.github/workflows/cargo-test.yaml @@ -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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 25b24150..db1da6b1 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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: diff --git a/Cargo.toml b/Cargo.toml index 581c93ea..f27783ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/__test__/canvas-class.spec.ts b/__test__/canvas-class.spec.ts index d56c4465..3cceb6a9 100644 --- a/__test__/canvas-class.spec.ts +++ b/__test__/canvas-class.spec.ts @@ -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) }) diff --git a/__test__/draw.spec.ts b/__test__/draw.spec.ts index 02eaccb3..d850cc8d 100644 --- a/__test__/draw.spec.ts +++ b/__test__/draw.spec.ts @@ -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) diff --git a/__test__/image-snapshot.ts b/__test__/image-snapshot.ts index 4eb8f1c3..fbf35787 100644 --- a/__test__/image-snapshot.ts +++ b/__test__/image-snapshot.ts @@ -13,7 +13,7 @@ const ARCH_NAME = arch() export async function snapshotImage( t: ExecutionContext, 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 diff --git a/__test__/snapshots/avif-output.avif b/__test__/snapshots/avif-output.avif new file mode 100644 index 0000000000000000000000000000000000000000..17de58e5aafb948c8e55d072522f24d7e23fbf90 GIT binary patch literal 3587 zcmeHIcTiL57Qdm3EKPcmB7%TR70{qkA_7Yhuuzw30)Ye~O;CF8VO78=geC|Gh@glp zy-E!bY5*fjuc24TCOA89=FPmBH}BuKXXbwAcYf!b@60{lch3a?fDh*Fk8<#Z!vKmP z;V?xc+<}5YE=Z`i1C^tQn9F^H=T8U#D7cgN-}rCMih_G1PYe)cy$DBmI8m(|06+s! z8Op<84*-n06#K*i#Q*@^Cy;7%y8m%o=c6zK(~#J~}72Ty;n z0|MpZKn)j$qmUy)!SBx@9) zlcC+DCR0Tdr~{3V*F^@VkKW~mMj_ODRboaJi2Q7yP^vRQSH6b54X3>=;Yc^Ovl5wC zK08==7`n#AuV1JN9gMMu`HR^$X1d-sLYttmvw8(Ri!F65;US z8Q%rMj$-SFcRh(~b1yWCN+tI8xnZRfU+_e`0nLvc`c|5otd%cU*;_F-8xhtB$gro0|=om{0|xvD5<`Z!GU1JgJNW;}BUoBXhd;-tunAQgVP)+R2qHojpn=IczT&H|1#HKu&~J0$aY za>35tgZghx>FJ{nc~;V1aVadUd)H%?^RbLZgrP%CJ?ue1fYXeYYM`UYwX+Q@tUkX_ zMWgWHQ;NW^t5OI(b1=`T{Jng0Bv4>VkO$}Q|rg_L|$sW7!76=vkyBYm!w~C{{3JPuBRv`TQ>N+RDxeSl? zOpmb_HXrR0P9LnP5+_Le*Rw1y^Gy-aG*7E_^F1c@m9NV3<;9Kd3%GI@1)w9}sQ5GGrqpp?m!q(JoEx zW2iv3idrg0h-Wg^JBxW}H83!8kmhsV2CFL#*G=-e6z97N{@&@mXFqDBSZ*#PV}_0Q zq6=<636{-Ul9DHMdzm_i@B%;2qO(r_+oOj~SBd)W^;C82%)c>cE@{HK0W=GrL@ z>Re-I?_*Ya%0?Y+8;Z$gh5!vWXg}m+xB-AD5Joe~-V#)I1y^k+=VdPQa*a5;@tanU z#24E)IqFNR*hk5u?$E_xA0e-b$vRbBl#S(`ytxj1>E?T*zH5fceG%XH<3e8_(g`Pp zrTQ7SIK+e$ljaQ86OHl$o{Q3K9p8aNyf&qmB(Oop-fpJC zzBu_%k}+ZO&M;P$#0_hP`m9`4uy_?TZ~58!F?TxxA8Ed*V)#Q|KBD*VURg+j8hD}G{}(cPZoav~MyzQoReI;mjS?aM;_0PQ-HQoNvYOQ_9Y*3Yd(Bq30+;Ibfsl_h zBgYzCRS){&(e!@CDaj0u_wZUy5E z=~6YNzl-`YxzCsnx=+|C7%J1mWpxSivelOtOssb=8w$C|xR-R+`bEbaUM)%M&bX^R zRdZe_K1pufU+qNfcr?82KHlI&_2xG7h zy^5&${?d zLlYBLts|U=ZJrUoD(!8$YW@{8XI+ZJQ%WL+A= z?Bj&u2MwO5?joNnF-t68uSnG?8Pk*m{{7|(e#t+qNDNkD encode(format: 'png'): Promise + encode(format: 'avif', cfg?: AvifConfig): Promise - 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 toDataURLAsync(mime: 'image/jpeg' | 'image/webp', quality?: number): Promise toDataURLAsync(mime?: 'image/jpeg' | 'image/webp' | 'image/png', quality?: number): Promise + toDataURLAsync(mime?: 'image/avif', cfg?: AvifConfig): Promise } export function createCanvas(width: number, height: number): Canvas diff --git a/index.js b/index.js index e009a182..c52f6f59 100644 --- a/index.js +++ b/index.js @@ -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 } diff --git a/musl.Dockerfile b/musl.Dockerfile index cccb26c0..703d1657 100644 --- a/musl.Dockerfile +++ b/musl.Dockerfile @@ -15,6 +15,7 @@ RUN apk add --update --no-cache musl-dev wget && \ git \ build-base \ clang \ + nasm \ llvm \ nasm \ gn \ diff --git a/package.json b/package.json index 62e0d35f..184f3e63 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "ava": { "require": ["@swc-node/register"], "extensions": ["ts"], - "timeout": "30s", + "timeout": "3m", "environmentVariables": { "SWC_NODE_PROJECT": "./tsconfig.json", "NODE_ENV": "ava" diff --git a/src/ctx.rs b/src/ctx.rs index e188347c..bcb2b72c 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -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::{ @@ -2128,29 +2130,50 @@ fn get_text_baseline(ctx: CallContext) -> Result { .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), +} + impl Task for ContextData { - type Output = SkiaDataRef; + type Output = ContextOutputData; type JsValue = JsBuffer; fn compute(&mut self) -> Result { 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, @@ -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 { - 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 { + 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()), } } } diff --git a/src/lib.rs b/src/lib.rs index ca82dccb..8191a427 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,12 +7,13 @@ extern crate napi_derive; #[macro_use] extern crate serde_derive; -use std::mem; +use std::{mem, slice}; use napi::*; -use ctx::{Context, ContextData, ImageOrCanvas}; +use ctx::{AVIFConfig, Context, ContextData, ContextOutputData, ImageOrCanvas}; use font::{init_font_regexp, FONT_REGEXP}; +use rgb::FromSlice; use sk::SkiaDataRef; #[cfg(all( @@ -42,6 +43,9 @@ mod svg; const MIME_WEBP: &str = "image/webp"; const MIME_PNG: &str = "image/png"; const MIME_JPEG: &str = "image/jpeg"; +const MIME_AVIF: &str = "image/avif"; + +const DEFAULT_IMAGE_QUALITY: u8 = 92; #[module_exports] fn init(mut exports: JsObject, env: Env) -> Result<()> { @@ -49,13 +53,27 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> { "CanvasElement", canvas_element_constructor, &[ - Property::new(&env, "encode")?.with_method(encode), - Property::new(&env, "encodeSync")?.with_method(encode_sync), - Property::new(&env, "toBuffer")?.with_method(to_buffer), - Property::new(&env, "savePNG")?.with_method(save_png), - Property::new(&env, "data")?.with_method(data), - Property::new(&env, "toDataURL")?.with_method(to_data_url), - Property::new(&env, "toDataURLAsync")?.with_method(to_data_url_async), + Property::new(&env, "encode")? + .with_method(encode) + .with_property_attributes(PropertyAttributes::Writable), + Property::new(&env, "encodeSync")? + .with_method(encode_sync) + .with_property_attributes(PropertyAttributes::Writable), + Property::new(&env, "toBuffer")? + .with_method(to_buffer) + .with_property_attributes(PropertyAttributes::Writable), + Property::new(&env, "savePNG")? + .with_method(save_png) + .with_property_attributes(PropertyAttributes::Writable), + Property::new(&env, "data")? + .with_method(data) + .with_property_attributes(PropertyAttributes::Writable), + Property::new(&env, "toDataURL")? + .with_method(to_data_url) + .with_property_attributes(PropertyAttributes::Writable), + Property::new(&env, "toDataURLAsync")? + .with_method(to_data_url_async) + .with_property_attributes(PropertyAttributes::Writable), ], )?; @@ -143,24 +161,29 @@ fn create_context(ctx: CallContext) -> Result { #[js_function(2)] fn encode(ctx: CallContext) -> Result { let format = ctx.get::(0)?.into_utf8()?; - let quality = if ctx.length == 1 { - 92 - } else { + let format_str = format.as_str()?; + let quality = if format_str != "avif" { ctx.get::(1)?.get_uint32()? as u8 + } else { + DEFAULT_IMAGE_QUALITY }; let this = ctx.this_unchecked::(); let ctx_js = this.get_named_property::("ctx")?; let ctx2d = ctx.env.unwrap::(&ctx_js)?; let surface_ref = ctx2d.surface.reference(); - let task = match format.as_str()? { + let task = match format_str { "webp" => ContextData::Webp(surface_ref, quality), "jpeg" => ContextData::Jpeg(surface_ref, quality), "png" => ContextData::Png(surface_ref), + "avif" => { + let cfg: AVIFConfig = serde_json::from_str(ctx.get::(1)?.into_utf8()?.as_str()?)?; + ContextData::Avif(surface_ref, cfg, ctx2d.width, ctx2d.height) + } _ => { return Err(Error::new( Status::InvalidArg, - format!("{} is not valid format", format.as_str()?), + format!("{} is not valid format", format_str), )) } }; @@ -171,24 +194,56 @@ fn encode(ctx: CallContext) -> Result { #[js_function(2)] fn encode_sync(ctx: CallContext) -> Result { let format = ctx.get::(0)?.into_utf8()?; - let quality = if ctx.length == 1 { - 100 - } else { + let format_str = format.as_str()?; + let quality = if format_str != "avif" { ctx.get::(1)?.get_uint32()? as u8 + } else { + DEFAULT_IMAGE_QUALITY }; let this = ctx.this_unchecked::(); let ctx_js = this.get_named_property::("ctx")?; let ctx2d = ctx.env.unwrap::(&ctx_js)?; let surface_ref = ctx2d.surface.reference(); - if let Some(data_ref) = match format.as_str()? { + if let Some(data_ref) = match format_str { "webp" => surface_ref.encode_data(sk::SkEncodedImageFormat::Webp, quality), "jpeg" => surface_ref.encode_data(sk::SkEncodedImageFormat::Jpeg, quality), "png" => surface_ref.png_data(), + "avif" => { + let (data, size) = surface_ref.data().ok_or_else(|| { + Error::new( + Status::GenericFailure, + "Encode to avif error, failed to get surface pixels".to_owned(), + ) + })?; + let config: AVIFConfig = + serde_json::from_str(ctx.get::(1)?.into_utf8()?.as_str()?)?; + let output = ravif::encode_rgba( + ravif::Img::new( + unsafe { slice::from_raw_parts(data, size) }.as_rgba(), + ctx2d.width as usize, + ctx2d.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)| o) + .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?; + return ctx + .env + .create_buffer_with_data(output) + .map(|b| b.into_raw()); + } _ => { return Err(Error::new( Status::InvalidArg, - format!("{} is not valid format", format.as_str()?), + format!("{} is not valid format", format_str), )) } } { @@ -213,29 +268,31 @@ fn encode_sync(ctx: CallContext) -> Result { #[js_function(2)] fn to_buffer(ctx: CallContext) -> Result { - let mime = if ctx.length == 0 { - MIME_PNG.to_owned() - } else { - let mime_js = ctx.get::(0)?.into_utf8()?; - mime_js.as_str()?.to_owned() - }; + let mime_js = ctx.get::(0)?.into_utf8()?; + let mime = mime_js.as_str()?; let quality = if ctx.length < 2 { - 92 + DEFAULT_IMAGE_QUALITY } else { ctx.get::(1)?.get_uint32()? as u8 }; - let data_ref = get_data_ref(&ctx, mime.as_str(), quality)?; - unsafe { - ctx + let context_data = get_data_ref(&ctx, mime, quality)?; + match context_data { + ContextOutputData::Skia(data_ref) => unsafe { + ctx + .env + .create_buffer_with_borrowed_data( + data_ref.0.ptr, + data_ref.0.size, + data_ref, + |data: SkiaDataRef, _| mem::drop(data), + ) + .map(|b| b.into_raw()) + }, + ContextOutputData::Avif(output) => ctx .env - .create_buffer_with_borrowed_data( - data_ref.0.ptr, - data_ref.0.size, - data_ref, - |data: SkiaDataRef, _| mem::drop(data), - ) - .map(|b| b.into_raw()) + .create_buffer_with_data(output) + .map(|b| b.into_raw()), } } @@ -263,42 +320,41 @@ fn data(ctx: CallContext) -> Result { #[js_function(2)] fn to_data_url(ctx: CallContext) -> Result { - let mime = if ctx.length == 0 { - MIME_PNG.to_owned() - } else { - let mime_js = ctx.get::(0)?.into_utf8()?; - mime_js.as_str()?.to_owned() - }; + let mime_js = ctx.get::(0)?.into_utf8()?; + let mime = mime_js.as_str()?; let quality = if ctx.length < 2 { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL - 92 + DEFAULT_IMAGE_QUALITY } else { ctx.get::(1)?.get_uint32()? as u8 }; - let data_ref = get_data_ref(&ctx, mime.as_str(), quality)?; + let data_ref = get_data_ref(&ctx, mime, quality)?; let mut output = format!("data:{};base64,", &mime); - base64::encode_config_buf(data_ref.slice(), base64::STANDARD, &mut output); + match data_ref { + ContextOutputData::Avif(data) => { + base64::encode_config_buf(data.as_slice(), base64::STANDARD, &mut output); + } + ContextOutputData::Skia(data_ref) => { + base64::encode_config_buf(data_ref.slice(), base64::STANDARD, &mut output); + } + } ctx.env.create_string_from_std(output) } #[js_function(2)] fn to_data_url_async(ctx: CallContext) -> Result { - let mime = if ctx.length == 0 { - MIME_PNG.to_owned() - } else { - let mime_js = ctx.get::(0)?.into_utf8()?; - mime_js.as_str()?.to_owned() - }; + let mime_js = ctx.get::(0)?.into_utf8()?; + let mime = mime_js.as_str()?; let quality = if ctx.length < 2 { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL - 92 + DEFAULT_IMAGE_QUALITY } else { ctx.get::(1)?.get_uint32()? as u8 }; - let data_ref = get_data_ref(&ctx, mime.as_str(), quality)?; + let data_ref = get_data_ref(&ctx, mime, quality)?; let async_task = AsyncDataUrl { surface_data: data_ref, - mime, + mime: mime.to_owned(), }; ctx.env.spawn(async_task).map(|p| p.promise_object()) } @@ -321,7 +377,7 @@ fn get_content(ctx: CallContext) -> Result { } } -fn get_data_ref(ctx: &CallContext, mime: &str, quality: u8) -> Result { +fn get_data_ref(ctx: &CallContext, mime: &str, quality: u8) -> Result { let this = ctx.this_unchecked::(); let ctx_js = this.get_named_property::("ctx")?; let ctx2d = ctx.env.unwrap::(&ctx_js)?; @@ -331,6 +387,34 @@ fn get_data_ref(ctx: &CallContext, mime: &str, quality: u8) -> Result surface_ref.encode_data(sk::SkEncodedImageFormat::Webp, quality), MIME_JPEG => surface_ref.encode_data(sk::SkEncodedImageFormat::Jpeg, quality), MIME_PNG => surface_ref.png_data(), + MIME_AVIF => { + let (data, size) = surface_ref.data().ok_or_else(|| { + Error::new( + Status::GenericFailure, + "Encode to avif error, failed to get surface pixels".to_owned(), + ) + })?; + let config: AVIFConfig = + serde_json::from_str(ctx.get::(1)?.into_utf8()?.as_str()?)?; + let output = ravif::encode_rgba( + ravif::Img::new( + unsafe { slice::from_raw_parts(data, size) }.as_rgba(), + ctx2d.width as usize, + ctx2d.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)| o) + .map_err(|e| Error::new(Status::GenericFailure, format!("{}", e)))?; + return Ok(ContextOutputData::Avif(output)); + } _ => { return Err(Error::new( Status::InvalidArg, @@ -338,7 +422,7 @@ fn get_data_ref(ctx: &CallContext, mime: &str, quality: u8) -> Result Result { } struct AsyncDataUrl { - surface_data: SkiaDataRef, + surface_data: ContextOutputData, mime: String, } @@ -370,7 +454,14 @@ impl Task for AsyncDataUrl { fn compute(&mut self) -> Result { let mut output = format!("data:{};base64,", &self.mime); - base64::encode_config_buf(self.surface_data.slice(), base64::URL_SAFE, &mut output); + match &self.surface_data { + ContextOutputData::Skia(data_ref) => { + base64::encode_config_buf(data_ref.slice(), base64::URL_SAFE, &mut output); + } + ContextOutputData::Avif(o) => { + base64::encode_config_buf(o.as_slice(), base64::URL_SAFE, &mut output); + } + } Ok(output) }