From 713ca40d0ec36052a201269dab155b3e07fd7dab Mon Sep 17 00:00:00 2001 From: LongYinan Date: Thu, 15 Aug 2024 11:17:30 +0800 Subject: [PATCH] fix: image decode should be async (#872) * fix: image decode should be async * set bitmap immutable --- .cargo/config.toml | 7 +- .taplo.toml | 1 + Cargo.toml | 40 +++-- __test__/draw.spec.ts | 43 +++++ __test__/filter.spec.ts | 22 ++- __test__/image-snapshot.ts | 2 - __test__/image.spec.ts | 39 ++++- __test__/regression.spec.ts | 5 + package.json | 4 +- skia-c/skia_c.cpp | 2 + src/image.rs | 303 ++++++++++++++++++++++++------------ src/sk.rs | 3 + yarn.lock | 8 + 13 files changed, 346 insertions(+), 133 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 3b754dde..839a50b5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -11,8 +11,5 @@ rustflags = ["-C", "target-cpu=cortex-a57"] rustflags = ["-C", "target-cpu=cortex-a7"] [target.aarch64-unknown-linux-musl] -linker = "aarch64-linux-musl-gcc" -rustflags = [ - "-C", - "target-cpu=cortex-a57", -] +linker = "aarch64-linux-musl-gcc" +rustflags = ["-C", "target-cpu=cortex-a57"] diff --git a/.taplo.toml b/.taplo.toml index df0b54ca..1957a688 100644 --- a/.taplo.toml +++ b/.taplo.toml @@ -5,3 +5,4 @@ exclude = ["node_modules/**/*.toml", "skia/**/*.toml"] align_entries = true indent_tables = true reorder_keys = true +column_width = 180 diff --git a/Cargo.toml b/Cargo.toml index 8b67f273..37bdd3de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,27 +8,23 @@ version = "0.1.0" crate-type = ["cdylib"] [dependencies] -anyhow = "1" -base64 = "0.22" -cssparser = "0.29" -infer = "0.16" -libavif = { version = "0.14", default-features = false, features = [ - "codec-aom", -] } -napi = { version = "2", default-features = false, features = [ - "napi3", - "serde-json", -] } -napi-derive = { version = "2", default-features = false } -nom = "7" -num_cpus = "1" -once_cell = "1" -regex = "1" -rgb = "0.8" -serde = "1" +anyhow = "1" +base64 = "0.22" +base64-simd = "0.8" +cssparser = "0.29" +infer = "0.16" +libavif = { version = "0.14", default-features = false, features = ["codec-aom"] } +napi = { version = "3.0.0-alpha.8", default-features = false, features = ["napi3", "serde-json"] } +napi-derive = { version = "3.0.0-alpha.7", default-features = false } +nom = "7" +num_cpus = "1" +once_cell = "1" +regex = "1" +rgb = "0.8" +serde = "1" serde_derive = "1" -serde_json = "1" -thiserror = "1" +serde_json = "1" +thiserror = "1" [target.'cfg(not(target_os = "linux"))'.dependencies] mimalloc = "0.1" @@ -41,6 +37,6 @@ cc = "1" napi-build = "2" [profile.release] -lto = true codegen-units = 1 -strip = "symbols" +lto = true +strip = "symbols" diff --git a/__test__/draw.spec.ts b/__test__/draw.spec.ts index 0edd6219..d2690796 100644 --- a/__test__/draw.spec.ts +++ b/__test__/draw.spec.ts @@ -240,7 +240,12 @@ test('createPattern-no-transform', async (t) => { const { ctx } = t.context const imageSrc = await promises.readFile(join(__dirname, 'canvas_createpattern.png')) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = imageSrc + await promise const pattern = ctx.createPattern(image, 'repeat') ctx.fillStyle = pattern ctx.fillRect(0, 0, 300, 300) @@ -262,7 +267,12 @@ test('createPattern-with-transform', async (t) => { const { ctx } = t.context const imageSrc = await promises.readFile(join(__dirname, 'canvas_createpattern.png')) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = imageSrc + await promise const pattern = ctx.createPattern(image, 'repeat') const matrix = new DOMMatrix() pattern.setTransform(matrix.rotate(-45).scale(1.5)) @@ -308,7 +318,12 @@ test('drawImage', async (t) => { const filePath = './javascript.png' const file = await promises.readFile(join(__dirname, filePath)) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise ctx.drawImage(image, 0, 0) await snapshotImage(t) }) @@ -318,7 +333,12 @@ test('drawImage-svg', async (t) => { const filePath = './mountain.svg' const file = await promises.readFile(join(__dirname, filePath)) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise ctx.drawImage(image, 0, 0) await snapshotImage(t) }) @@ -328,7 +348,12 @@ test('drawImage-svg-with-only-viewBox', async (t) => { const filePath = './only-viewbox.svg' const file = await promises.readFile(join(__dirname, filePath)) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise ctx.drawImage(image, 0, 0) await snapshotImage(t) }) @@ -338,7 +363,12 @@ test('drawImage-svg-resize', async (t) => { const filePath = './resize.svg' const file = await promises.readFile(join(__dirname, filePath)) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise image.width = 100 image.height = 100 ctx.drawImage(image, 0, 0) @@ -360,7 +390,15 @@ test('drawImage-svg without width height should be empty image', async (t) => { const filePath = './mountain.svg' const svgContent = (await promises.readFile(join(__dirname, filePath))).toString('utf-8') const image = new Image() + const { promise, resolve, reject } = Promise.withResolvers() + image.onload = () => { + resolve() + } + image.onerror = (err) => { + reject(err) + } image.src = Buffer.from(svgContent.replace('width="128"', '').replace('height="128"', '')) + await promise ctx.drawImage(image, 0, 0) const output = await canvas.encode('png') const outputData = png.decoders['image/png'](output) @@ -372,7 +410,12 @@ test('draw-image-svg-noto-emoji', async (t) => { const filePath = './notoemoji-person.svg' const file = await promises.readFile(join(__dirname, filePath)) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise ctx.drawImage(image, 0, 0) await snapshotImage(t) }) diff --git a/__test__/filter.spec.ts b/__test__/filter.spec.ts index c409432c..3517cff8 100644 --- a/__test__/filter.spec.ts +++ b/__test__/filter.spec.ts @@ -14,12 +14,17 @@ const test = ava as TestFn<{ const FIREFOX = readFileSync(join(__dirname, 'fixtures', 'firefox-logo.svg')) const FIREFOX_IMAGE = new Image(200, 206.433) +const { promise: firefoxImageLoad, resolve } = Promise.withResolvers() +FIREFOX_IMAGE.onload = () => { + resolve() +} FIREFOX_IMAGE.src = FIREFOX -test.beforeEach((t) => { +test.beforeEach(async (t) => { const canvas = createCanvas(300, 300) t.context.canvas = canvas t.context.ctx = canvas.getContext('2d') + await firefoxImageLoad }) test('filter-blur', async (t) => { @@ -33,7 +38,12 @@ test('filter-brightness', async (t) => { const { ctx } = t.context ctx.filter = 'brightness(2)' const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = await fs.readFile(join(__dirname, 'fixtures', 'filter-brightness.jpg')) + await promise ctx.drawImage(image, 0, 0) await snapshotImage(t) }) @@ -42,7 +52,12 @@ test('filter-contrast', async (t) => { const { ctx } = t.context ctx.filter = 'contrast(200%)' const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = await fs.readFile(join(__dirname, 'fixtures', 'filter-contrast.jpeg')) + await promise ctx.drawImage(image, 0, 0) await snapshotImage(t) }) @@ -122,6 +137,11 @@ test('filter-save-restore', async (t) => { async function createImage(name: string) { const i = new Image() + const { promise, resolve } = Promise.withResolvers() + i.onload = () => { + resolve() + } i.src = await fs.readFile(join(__dirname, 'fixtures', name)) + await promise return i } diff --git a/__test__/image-snapshot.ts b/__test__/image-snapshot.ts index 199f98f8..2b0faafd 100644 --- a/__test__/image-snapshot.ts +++ b/__test__/image-snapshot.ts @@ -47,7 +47,6 @@ export async function snapshotImage( if (existedPixels.length !== imagePixels.length) { await writeFailureImage() t.fail('Image size is not equal') - return } let diffCount = 0 imagePixels.forEach((u8, index) => { @@ -58,7 +57,6 @@ export async function snapshotImage( if (diffCount / existedPixels.length > differentRatio / 100) { await writeFailureImage() t.fail(`Image bytes is not equal, different ratio is ${((diffCount / existedPixels.length) * 100).toFixed(2)}%`) - return } t.pass('Image pixels is equal') } diff --git a/__test__/image.spec.ts b/__test__/image.spec.ts index dd78d5c5..34cd8bab 100644 --- a/__test__/image.spec.ts +++ b/__test__/image.spec.ts @@ -16,16 +16,29 @@ test('should be able to create Image', (t) => { test('should be able to set src with buffer', async (t) => { const file = await loadImageFile() - t.notThrows(() => { + await t.notThrowsAsync(async () => { const image = new Image() + const { promise, resolve, reject } = Promise.withResolvers() + image.onload = () => { + resolve() + } + image.onerror = (err) => { + reject(err) + } image.src = file + await promise }) }) test('width and height state should be ok', async (t) => { const file = await loadImageFile() const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise t.is(image.width, 300) t.is(image.height, 320) t.is(image.naturalWidth, 300) @@ -36,8 +49,13 @@ test('width and height state should be ok', async (t) => { test('complete state should be ok', async (t) => { const file = await loadImageFile() const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } t.is(image.complete, false) image.src = file + await promise t.is(image.complete, true) }) @@ -51,7 +69,12 @@ test('alt state should be ok', (t) => { test('with-exif image width and height should be correct', async (t) => { const file = await fs.readFile(join(__dirname, 'fixtures', 'with-exif.jpg')) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise t.is(image.width, 450) t.is(image.height, 600) }) @@ -59,7 +82,12 @@ test('with-exif image width and height should be correct', async (t) => { test('draw-image-exif', async (t) => { const file = await fs.readFile(join(__dirname, 'fixtures', 'with-exif.jpg')) const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = file + await promise const canvas = createCanvas(800, 800) const ctx = canvas.getContext('2d') ctx.drawImage(image, 0, 0) @@ -86,7 +114,12 @@ test('properties should be readonly', (t) => { test('svg-transparent-background', async (t) => { const image = new Image() + const { promise, resolve } = Promise.withResolvers() + image.onload = () => { + resolve() + } image.src = await fs.readFile(join(__dirname, '..', 'example', 'resize-svg.svg')) + await promise const w = 1000 const h = 1000 @@ -117,11 +150,9 @@ test('load invalid image should not throw if onerror is provided', async (t) => () => new Promise((resolve) => { const image = new Image() - image.onload = () => { - resolve() - } image.onerror = (err) => { t.is(err.message, 'Unsupported image type') + resolve() } image.src = broken }), diff --git a/__test__/regression.spec.ts b/__test__/regression.spec.ts index 698f5e62..f88077c6 100644 --- a/__test__/regression.spec.ts +++ b/__test__/regression.spec.ts @@ -186,7 +186,12 @@ test('draw-svg-with-text', async (t) => { ctx.fillText(Title, 80, 100) const Arrow = new Image() + const { promise, resolve } = Promise.withResolvers() + Arrow.onload = () => { + resolve() + } Arrow.src = await fs.readFile(join(__dirname, 'image-og.svg')) + await promise ctx.drawImage(Arrow, 80, 60) await snapshotImage(t, { ctx, canvas }, 'png', process.arch === 'x64' && process.platform !== 'darwin' ? 0.15 : 0.3) }) diff --git a/package.json b/package.json index 2b0eed00..8a11430b 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "canvaskit-wasm": "^0.39.1", "colorette": "^2.0.20", "conventional-changelog-cli": "^5.0.0", + "core-js": "^3.38.0", "echarts": "^5.4.3", "husky": "^9.0.10", "lint-staged": "^15.2.1", @@ -109,7 +110,8 @@ }, "ava": { "require": [ - "@swc-node/register" + "@swc-node/register", + "core-js/proposals/promise-with-resolvers.js" ], "extensions": [ "ts" diff --git a/skia-c/skia_c.cpp b/skia-c/skia_c.cpp index 8427ea69..5c186b0a 100644 --- a/skia-c/skia_c.cpp +++ b/skia-c/skia_c.cpp @@ -1511,9 +1511,11 @@ extern "C" canvas->setMatrix(matrix); auto image = SkImages::RasterFromBitmap(*bitmap); canvas->drawImage(image, 0, 0); + oriented_bitmap->setImmutable(); bitmap_info->bitmap = reinterpret_cast(oriented_bitmap); delete bitmap; } else { + bitmap->setImmutable(); bitmap_info->bitmap = reinterpret_cast(bitmap); } bitmap_info->width = width; diff --git a/src/image.rs b/src/image.rs index d68aeb7b..d6df8e86 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,8 +1,8 @@ -use std::str; use std::str::FromStr; +use std::{ptr, str}; -use base64::{engine::general_purpose::STANDARD, Engine}; -use napi::{bindgen_prelude::*, NapiValue}; +use base64_simd::STANDARD; +use napi::{bindgen_prelude::*, check_status, NapiRaw, NapiValue, Ref}; use crate::error::SkError; use crate::global_fonts::get_font; @@ -124,7 +124,7 @@ pub struct Image { pub(crate) need_regenerate_bitmap: bool, pub(crate) is_svg: bool, pub(crate) color_space: ColorSpace, - pub(crate) src: Option, + pub(crate) src: Option, } #[napi] @@ -209,154 +209,261 @@ impl Image { } #[napi(getter)] - pub fn get_src(&mut self) -> Option<&mut Buffer> { + pub fn get_src(&mut self) -> Option<&mut Uint8Array> { self.src.as_mut() } #[napi(setter)] - pub fn set_src(&mut self, env: Env, this: This, data: Buffer) -> Result<()> { + pub fn set_src(&mut self, env: Env, this: This, data: Uint8Array) -> Result<()> { let length = data.len(); if length <= 2 { self.src = Some(data); - self.on_load(&this)?; + let onload = this.get_named_property_unchecked::("onload")?; + if onload.get_type()? == ValueType::Function { + let onload_func: Function<(), ()> = Function::from_unknown(onload)?; + onload_func.apply(this, ())?; + } + return Ok(()); + } + let on_error_in_catch = { + let on_error = this.get_named_property_unchecked::("onerror")?; + if on_error.get_type()? == ValueType::Function { + let onerror_func: Function = Function::from_unknown(on_error)?; + Some(onerror_func.create_ref()?) + } else { + None + } + }; + let decoder = BitmapDecoder { + width: self.width, + height: self.height, + color_space: self.color_space, + data, + this_ref: env.create_reference(&this)?, + }; + let task_output = env.spawn(decoder)?; + + task_output + .promise_object() + .catch(move |ctx: CallbackContext| { + if let Some(on_error) = on_error_in_catch { + let on_err = on_error.borrow_back(&ctx.env)?; + on_err.call(ctx.value)?; + } + Ok(()) + })?; + Ok(()) + } + + pub(crate) fn regenerate_bitmap_if_need(&mut self) -> Result<()> { + if !self.need_regenerate_bitmap || !self.is_svg || self.src.is_none() { return Ok(()); } - let data_ref: &[u8] = &data; - self.complete = true; - self.is_svg = false; + if let Some(data) = self.src.as_mut() { + let font = get_font().map_err(SkError::from)?; + self.bitmap = Bitmap::from_svg_data_with_custom_size( + data.as_ref().as_ptr(), + data.as_ref().len(), + self.width as f32, + self.height as f32, + self.color_space, + &font, + ); + } + Ok(()) + } +} + +fn is_svg_image(data: &[u8], length: usize) -> bool { + let mut is_svg = false; + if length >= 11 { + for i in 3..length { + if '<' == data[i - 3] as char { + match data[i - 2] as char { + '?' | '!' => continue, + 's' => { + is_svg = 'v' == data[i - 1] as char && 'g' == data[i] as char; + break; + } + _ => { + is_svg = false; + } + } + } + } + } + is_svg +} + +struct BitmapDecoder { + width: f64, + height: f64, + color_space: ColorSpace, + data: Uint8Array, + this_ref: Ref<()>, +} + +#[derive(Debug)] +pub(crate) struct DecodedBitmap { + bitmap: DecodeStatus, + width: f64, + height: f64, +} + +#[derive(Debug)] +struct BitmapInfo { + data: Bitmap, + is_svg: bool, +} + +#[derive(Debug)] +enum DecodeStatus { + Ok(BitmapInfo), + Empty, + InvalidSvg, + InvalidImage, +} + +impl Task for BitmapDecoder { + type Output = DecodedBitmap; + type JsValue = (); + + fn compute(&mut self) -> Result { + let length = self.data.len(); + let data_ref: &[u8] = &self.data; + let mut width = self.width; + let mut height = self.height; let bitmap = if str::from_utf8(&data_ref[0..10]) == Ok("data:image") { let data_str = str::from_utf8(data_ref) .map_err(|e| Error::new(Status::InvalidArg, format!("Decode data url failed {e}")))?; if let Some(base64_str) = data_str.split(',').last() { let image_binary = STANDARD - .decode(base64_str) + .decode_to_vec(base64_str) .map_err(|e| Error::new(Status::InvalidArg, format!("Decode data url failed {e}")))?; if let Some(kind) = infer::get(&image_binary) { if kind.matcher_type() == infer::MatcherType::Image { - Some(Bitmap::from_buffer( - image_binary.as_ptr() as *mut u8, - image_binary.len(), - )) + DecodeStatus::Ok(BitmapInfo { + data: Bitmap::from_buffer(image_binary.as_ptr().cast_mut(), image_binary.len()), + is_svg: false, + }) } else { - self.on_error(env, &this, None)?; - None + DecodeStatus::InvalidImage } } else { - self.on_error(env, &this, None)?; - None + DecodeStatus::InvalidImage } } else { - None + DecodeStatus::Empty } - } else if let Some(kind) = infer::get(&data) + } else if let Some(kind) = infer::get(&self.data) && kind.matcher_type() == infer::MatcherType::Image { - Some(Bitmap::from_buffer(data.as_ptr() as *mut u8, length)) - } else if self.is_svg_image(data_ref, length) { - self.is_svg = true; + DecodeStatus::Ok(BitmapInfo { + data: Bitmap::from_buffer(self.data.as_ptr().cast_mut(), length), + is_svg: false, + }) + } else if is_svg_image(data_ref, length) { let font = get_font().map_err(SkError::from)?; if (self.width - -1.0).abs() > f64::EPSILON && (self.height - -1.0).abs() > f64::EPSILON { if let Some(bitmap) = Bitmap::from_svg_data_with_custom_size( - data.as_ptr(), + self.data.as_ptr(), length, self.width as f32, self.height as f32, self.color_space, &font, ) { - Some(bitmap) + DecodeStatus::Ok(BitmapInfo { + data: bitmap, + is_svg: true, + }) } else { - self.on_error(env, &this, Some("Invalid SVG image"))?; - None + DecodeStatus::InvalidSvg } } else if let Some(bitmap) = - Bitmap::from_svg_data(data.as_ptr(), length, self.color_space, &font) + Bitmap::from_svg_data(self.data.as_ptr(), length, self.color_space, &font) { if let Ok(bitmap) = bitmap { - Some(bitmap) + DecodeStatus::Ok(BitmapInfo { + data: bitmap, + is_svg: true, + }) } else { - self.on_error(env, &this, Some("Invalid SVG image"))?; - None + DecodeStatus::InvalidSvg } } else { - None + DecodeStatus::Empty } } else { - self.on_error(env, &this, None)?; - None + DecodeStatus::InvalidImage }; - if let Some(ref b) = bitmap { + + if let DecodeStatus::Ok(ref b) = bitmap { if (self.width - -1.0).abs() < f64::EPSILON { - self.width = b.0.width as f64; + width = b.data.0.width as f64; } if (self.height - -1.0).abs() < f64::EPSILON { - self.height = b.0.height as f64; + height = b.data.0.height as f64; } } - self.bitmap = bitmap; - self.src = Some(data); - self.on_load(&this)?; - Ok(()) - } - - pub(crate) fn regenerate_bitmap_if_need(&mut self) -> Result<()> { - if !self.need_regenerate_bitmap || !self.is_svg || self.src.is_none() { - return Ok(()); - } - if let Some(data) = self.src.as_mut() { - let font = get_font().map_err(SkError::from)?; - self.bitmap = Bitmap::from_svg_data_with_custom_size( - data.as_ref().as_ptr(), - data.as_ref().len(), - self.width as f32, - self.height as f32, - self.color_space, - &font, - ); - } - Ok(()) - } - - fn on_load(&self, this: &This) -> Result<()> { - let onload = this.get_named_property_unchecked::("onload")?; - if onload.get_type()? == ValueType::Function { - let onload_func = unsafe { onload.cast::() }; - onload_func.call_without_args(Some(this))?; - } - Ok(()) - } - - fn on_error(&self, env: Env, this: &This, msg: Option<&str>) -> Result<()> { - let onerror = this.get_named_property_unchecked::("onerror")?; - let reason = msg.unwrap_or("Unsupported image type"); - let err = Error::new(Status::InvalidArg, reason); - if onerror.get_type()? == ValueType::Function { - let onerror_func = unsafe { onerror.cast::() }; - onerror_func.call(Some(this), &[JsError::from(err).into_unknown(env)])?; - Ok(()) - } else { - Err(err) - } + Ok(DecodedBitmap { + bitmap, + width, + height, + }) } - fn is_svg_image(&self, data: &[u8], length: usize) -> bool { - let mut is_svg = false; - if length >= 11 { - for i in 3..length { - if '<' == data[i - 3] as char { - match data[i - 2] as char { - '?' | '!' => continue, - 's' => { - is_svg = 'v' == data[i - 1] as char && 'g' == data[i] as char; - break; - } - _ => { - is_svg = false; - } - } + fn resolve(&mut self, env: Env, output: Self::Output) -> Result { + let this: This = env.get_reference_value(&self.this_ref)?; + let mut image_ptr = ptr::null_mut(); + check_status!( + unsafe { sys::napi_unwrap(env.raw(), this.raw(), &mut image_ptr) }, + "Failed to unwrap Image from this" + )?; + let self_mut = unsafe { Box::leak(Box::from_raw(image_ptr.cast::())) }; + self_mut.width = output.width; + self_mut.height = output.height; + self_mut.complete = true; + match output.bitmap { + DecodeStatus::Ok(bitmap) => { + let onload = this.get_named_property_unchecked::("onload")?; + if onload.get_type()? == ValueType::Function { + let onload_func: Function<(), ()> = Function::from_unknown(onload)?; + onload_func.apply(this, ())?; + self_mut.src = Some(self.data.clone()); + } + self_mut.is_svg = bitmap.is_svg; + self_mut.bitmap = Some(bitmap.data); + } + DecodeStatus::Empty => { + let onload = this.get_named_property_unchecked::("onload")?; + if onload.get_type()? == ValueType::Function { + let onload_func: Function<(), ()> = Function::from_unknown(onload)?; + onload_func.apply(this, ())?; + } + self_mut.bitmap = None; + } + DecodeStatus::InvalidSvg => { + let on_error = this.get_named_property_unchecked::("onerror")?; + if on_error.get_type()? == ValueType::Function { + let onerror_func: Function = Function::from_unknown(on_error)?; + onerror_func.apply( + this, + JsError::from(Error::new(Status::InvalidArg, "Invalid SVG image")).into_unknown(env), + )?; + } + } + DecodeStatus::InvalidImage => { + let on_error = this.get_named_property_unchecked::("onerror")?; + if on_error.get_type()? == ValueType::Function { + let onerror_func: Function = Function::from_unknown(on_error)?; + onerror_func.apply( + this, + env.create_error(Error::new(Status::InvalidArg, "Unsupported image type"))?, + )?; } } } - is_svg + Ok(()) } } diff --git a/src/sk.rs b/src/sk.rs index 67d98757..53863a79 100644 --- a/src/sk.rs +++ b/src/sk.rs @@ -3412,6 +3412,9 @@ impl Drop for ImageFilter { #[derive(Debug)] pub(crate) struct Bitmap(pub(crate) ffi::skiac_bitmap_info); +unsafe impl Send for Bitmap {} +unsafe impl Sync for Bitmap {} + impl Bitmap { pub fn from_buffer(ptr: *mut u8, size: usize) -> Self { let mut bitmap_info = ffi::skiac_bitmap_info { diff --git a/yarn.lock b/yarn.lock index e670bf4d..3cc93476 100644 --- a/yarn.lock +++ b/yarn.lock @@ -244,6 +244,7 @@ __metadata: canvaskit-wasm: "npm:^0.39.1" colorette: "npm:^2.0.20" conventional-changelog-cli: "npm:^5.0.0" + core-js: "npm:^3.38.0" echarts: "npm:^5.4.3" husky: "npm:^9.0.10" lint-staged: "npm:^15.2.1" @@ -1700,6 +1701,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^3.38.0": + version: 3.38.0 + resolution: "core-js@npm:3.38.0" + checksum: 10c0/3218ae19bfe0c6560663012cbd3e7f3dc1b36d50fc71e8c365f3b119185e8a35ac4e8bb9698ae510b3c201ef93f40bdc29f9215716ccf31aca28f77969bb4ed0 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3"