Skip to content

Commit

Permalink
feat: support toDataURL and toDataURLAsync on canvas element
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed Jul 14, 2021
1 parent 2673d67 commit 1d8c790
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 63 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -11,6 +11,7 @@ crate-type = ["cdylib"]

[dependencies]
anyhow = "1.0"
base64 = "0.13"
cssparser = "0.28"
napi = "1"
napi-derive = "1"
Expand Down
75 changes: 47 additions & 28 deletions __test__/draw.spec.ts
Expand Up @@ -775,38 +775,62 @@ test('transform', async (t) => {

test('translate', async (t) => {
const { ctx } = t.context
// Moved square
ctx.translate(110, 30)
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 80, 80)

// Reset current transformation matrix to the identity matrix
ctx.setTransform(1, 0, 0, 1, 0, 0)

// Unmoved square
ctx.fillStyle = 'gray'
ctx.fillRect(0, 0, 80, 80)
drawHouse(ctx)
await snapshotImage(t)
})

test('webp-output', async (t) => {
const { ctx } = t.context
// Moved square
ctx.translate(110, 30)
ctx.fillStyle = 'red'
ctx.fillRect(0, 0, 80, 80)

// Reset current transformation matrix to the identity matrix
ctx.setTransform(1, 0, 0, 1, 0, 0)

// Unmoved square
ctx.fillStyle = 'gray'
ctx.fillRect(0, 0, 80, 80)
drawHouse(ctx)
await snapshotImage(t, t.context, 'webp')
})

test('raw output', async (t) => {
const { ctx, canvas } = t.context
drawHouse(ctx)

const output = canvas.data()
const pngFromCanvas = await canvas.encode('png')
const pngOutput = png.decoders['image/png'](pngFromCanvas)
t.deepEqual(output, pngOutput.data)
})

test('toDataURL', async (t) => {
const { ctx, canvas } = t.context
drawHouse(ctx)

const output = canvas.toDataURL()
const prefix = 'data:image/png;base64,'
t.true(output.startsWith(prefix))
const imageBase64 = output.substr(prefix.length)
const pngBuffer = Buffer.from(imageBase64, 'base64')
t.deepEqual(pngBuffer, await canvas.encode('png'))
})

test('toDataURL with quality', async (t) => {
const { ctx, canvas } = t.context
drawHouse(ctx)

const output = canvas.toDataURL('image/jpeg', 20)
const prefix = 'data:image/jpeg;base64,'
t.true(output.startsWith(prefix))
const imageBase64 = output.substr(prefix.length)
const pngBuffer = Buffer.from(imageBase64, 'base64')
t.deepEqual(pngBuffer, await canvas.encode('jpeg', 20))
})

test('toDataURLAsync', async (t) => {
const { ctx, canvas } = t.context
drawHouse(ctx)
const output = await canvas.toDataURLAsync()
const prefix = 'data:image/png;base64,'
t.true(output.startsWith(prefix))
const imageBase64 = output.substr(prefix.length)
const pngBuffer = Buffer.from(imageBase64, 'base64')
t.deepEqual(pngBuffer, await canvas.encode('png'))
})

function drawHouse(ctx: SKRSContext2D) {
// Moved square
ctx.translate(110, 30)
ctx.fillStyle = 'red'
Expand All @@ -818,9 +842,4 @@ test('raw output', async (t) => {
// Unmoved square
ctx.fillStyle = 'gray'
ctx.fillRect(0, 0, 80, 80)

const output = canvas.data()
const pngFromCanvas = await canvas.encode('png')
const pngOutput = png.decoders['image/png'](pngFromCanvas)
t.deepEqual(output, pngOutput.data)
})
}
14 changes: 11 additions & 3 deletions index.d.ts
Expand Up @@ -277,13 +277,21 @@ export interface Canvas {
width: number
height: number
getContext(contextType: '2d', contextAttributes?: { alpha: boolean }): SKRSContext2D
encodeSync(format: 'webp' | 'jpeg', quality: number): Buffer
encodeSync(format: 'webp' | 'jpeg', quality?: number): Buffer
encodeSync(format: 'png'): Buffer
encode(format: 'webp' | 'jpeg', quality?: number): Promise<Buffer>
encode(format: 'png'): Promise<Buffer>

toBuffer(mime: 'image/png' | 'image/jpeg' | 'image/webp'): Buffer
// raw pixels
data(): Buffer
encode(format: 'webp' | 'jpeg', quality: number): Promise<Buffer>
encode(format: 'png'): Promise<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

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>
}

export function createCanvas(width: number, height: number): Canvas
Expand Down
139 changes: 107 additions & 32 deletions src/lib.rs
Expand Up @@ -35,6 +35,10 @@ mod pattern;
mod sk;
mod state;

const MIME_WEBP: &str = "image/webp";
const MIME_PNG: &str = "image/png";
const MIME_JPEG: &str = "image/jpeg";

#[module_exports]
fn init(mut exports: JsObject, env: Env) -> Result<()> {
let canvas_element = env.define_class(
Expand All @@ -47,6 +51,8 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> {
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),
],
)?;

Expand Down Expand Up @@ -133,7 +139,7 @@ fn get_context(ctx: CallContext) -> Result<JsObject> {
fn encode(ctx: CallContext) -> Result<JsObject> {
let format = ctx.get::<JsString>(0)?.into_utf8()?;
let quality = if ctx.length == 1 {
100
92
} else {
ctx.get::<JsNumber>(1)?.get_uint32()? as u8
};
Expand Down Expand Up @@ -208,38 +214,18 @@ fn to_buffer(ctx: CallContext) -> Result<JsBuffer> {
} else {
ctx.get::<JsNumber>(1)?.get_uint32()? as u8
};
let this = ctx.this_unchecked::<JsObject>();
let ctx_js = this.get_named_property::<JsObject>("ctx")?;
let ctx2d = ctx.env.unwrap::<Context>(&ctx_js)?;
let surface_ref = ctx2d.surface.reference();

if let Some(data_ref) = match mime.as_str()? {
"image/webp" => surface_ref.encode_data(sk::SkEncodedImageFormat::Webp, quality),
"image/jpeg" => surface_ref.encode_data(sk::SkEncodedImageFormat::Jpeg, quality),
"image/png" => surface_ref.png_data(),
_ => {
return Err(Error::new(
Status::InvalidArg,
format!("{} is not valid mime", mime.as_str()?),
))
}
} {
unsafe {
ctx
.env
.create_buffer_with_borrowed_data(
data_ref.0.ptr,
data_ref.0.size,
data_ref,
|data: SurfaceDataRef, _| mem::drop(data),
)
.map(|b| b.into_raw())
}
} else {
Err(Error::new(
Status::InvalidArg,
format!("encode {} output failed", mime.as_str()?),
))
let data_ref = get_data_ref(&ctx, mime.as_str()?, quality)?;
unsafe {
ctx
.env
.create_buffer_with_borrowed_data(
data_ref.0.ptr,
data_ref.0.size,
data_ref,
|data: SurfaceDataRef, _| mem::drop(data),
)
.map(|b| b.into_raw())
}
}

Expand All @@ -265,6 +251,75 @@ fn data(ctx: CallContext) -> Result<JsBuffer> {
}
}

#[js_function(2)]
fn to_data_url(ctx: CallContext) -> Result<JsString> {
let mime = if ctx.length == 0 {
MIME_PNG.to_owned()
} else {
let mime_js = ctx.get::<JsString>(0)?.into_utf8()?;
mime_js.as_str()?.to_owned()
};
let quality = if ctx.length < 2 {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
92
} else {
ctx.get::<JsNumber>(1)?.get_uint32()? as u8
};
let data_ref = get_data_ref(&ctx, mime.as_str(), quality)?;
let mut output = format!("data:{};base64,", &mime);
base64::encode_config_buf(data_ref.slice(), base64::URL_SAFE, &mut output);
ctx.env.create_string_from_std(output)
}

#[js_function(2)]
fn to_data_url_async(ctx: CallContext) -> Result<JsObject> {
let mime = if ctx.length == 0 {
MIME_PNG.to_owned()
} else {
let mime_js = ctx.get::<JsString>(0)?.into_utf8()?;
mime_js.as_str()?.to_owned()
};
let quality = if ctx.length < 2 {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL
92
} else {
ctx.get::<JsNumber>(1)?.get_uint32()? as u8
};
let data_ref = get_data_ref(&ctx, mime.as_str(), quality)?;
let async_task = AsyncDataUrl {
surface_data: data_ref,
mime,
};
ctx.env.spawn(async_task).map(|p| p.promise_object())
}

#[inline]
fn get_data_ref(ctx: &CallContext, mime: &str, quality: u8) -> Result<SurfaceDataRef> {
let this = ctx.this_unchecked::<JsObject>();
let ctx_js = this.get_named_property::<JsObject>("ctx")?;
let ctx2d = ctx.env.unwrap::<Context>(&ctx_js)?;
let surface_ref = ctx2d.surface.reference();

if let Some(data_ref) = match mime {
MIME_WEBP => surface_ref.encode_data(sk::SkEncodedImageFormat::Webp, quality),
MIME_JPEG => surface_ref.encode_data(sk::SkEncodedImageFormat::Jpeg, quality),
MIME_PNG => surface_ref.png_data(),
_ => {
return Err(Error::new(
Status::InvalidArg,
format!("{} is not valid mime", mime),
))
}
} {
Ok(data_ref)
} else {
Err(Error::new(
Status::InvalidArg,
format!("encode {} output failed", mime),
))
}
}

#[js_function(1)]
fn save_png(ctx: CallContext) -> Result<JsUndefined> {
let this = ctx.this_unchecked::<JsObject>();
Expand All @@ -276,3 +331,23 @@ fn save_png(ctx: CallContext) -> Result<JsUndefined> {

ctx.env.get_undefined()
}

struct AsyncDataUrl {
surface_data: SurfaceDataRef,
mime: String,
}

impl Task for AsyncDataUrl {
type Output = String;
type JsValue = JsString;

fn compute(&mut self) -> Result<Self::Output> {
let mut output = format!("data:{};base64,", &self.mime);
base64::encode_config_buf(self.surface_data.slice(), base64::URL_SAFE, &mut output);
Ok(output)
}

fn resolve(self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
env.create_string_from_std(output)
}
}

1 comment on commit 1d8c790

@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: 1d8c790 Previous: 2673d67 Ratio
Draw house#skia-canvas 21.4 ops/sec (±0.43%) 28 ops/sec (±0.13%) 1.31
Draw house#node-canvas 21.9 ops/sec (±0.81%) 21 ops/sec (±0.36%) 0.96
Draw house#@napi-rs/skia 21.2 ops/sec (±0.69%) 25 ops/sec (±0.11%) 1.18
Draw gradient#skia-canvas 19.5 ops/sec (±3.07%) 27 ops/sec (±0.08%) 1.38
Draw gradient#node-canvas 20.1 ops/sec (±1.22%) 21 ops/sec (±0.13%) 1.04
Draw gradient#@napi-rs/skia 19.9 ops/sec (±1.22%) 24 ops/sec (±0.08%) 1.21

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

Please sign in to comment.