From f95f67afd5c051c1cf33e783e452d477a9430cad Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 30 Jul 2021 18:56:44 +0800 Subject: [PATCH] feat: svg canvas backend --- __test__/svg-canvas.spec.ts | 40 ++++++++ __test__/svg-canvas.spec.ts.md | 26 +++++ __test__/svg-canvas.spec.ts.snap | Bin 0 -> 8390 bytes example/export-svg.js | 34 ++++++ example/export-text.svg | 9 ++ index.d.ts | 16 +++ index.js | 23 +++-- rust-toolchain | 2 +- skia-c/skia_c.cpp | 46 ++++++++- skia-c/skia_c.hpp | 15 ++- src/ctx.rs | 46 ++++++++- src/error.rs | 2 + src/lib.rs | 29 ++++++ src/sk.rs | 171 +++++++++++++++++++++++++++++++ 14 files changed, 444 insertions(+), 15 deletions(-) create mode 100644 __test__/svg-canvas.spec.ts create mode 100644 __test__/svg-canvas.spec.ts.md create mode 100644 __test__/svg-canvas.spec.ts.snap create mode 100644 example/export-svg.js create mode 100644 example/export-text.svg diff --git a/__test__/svg-canvas.spec.ts b/__test__/svg-canvas.spec.ts new file mode 100644 index 00000000..8e062f29 --- /dev/null +++ b/__test__/svg-canvas.spec.ts @@ -0,0 +1,40 @@ +import { join } from 'path' + +import ava, { TestInterface } from 'ava' + +import { createCanvas, SvgCanvas, SvgExportFlag, GlobalFonts } from '../index' + +const test = ava as TestInterface<{ + canvas: SvgCanvas +}> + +test.beforeEach((t) => { + t.context.canvas = createCanvas(1024, 768, SvgExportFlag.ConvertTextToPaths) +}) + +test('should be able to export path/arc/rect', (t) => { + const { canvas } = t.context + const ctx = canvas.getContext('2d') + ctx.fillStyle = 'yellow' + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.lineWidth = 3 + ctx.strokeStyle = 'hotpink' + ctx.strokeRect(50, 450, 100, 100) + ctx.fillStyle = 'hotpink' + ctx.arc(500, 120, 90, 0, Math.PI * 2) + ctx.fill() + t.snapshot(canvas.getContent().toString('utf8')) +}) + +test('should be able to export text', (t) => { + GlobalFonts.registerFromPath(join(__dirname, 'fonts-dir', 'iosevka-curly-regular.woff2'), 'i-curly') + const { canvas } = t.context + const ctx = canvas.getContext('2d') + ctx.fillStyle = 'yellow' + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.lineWidth = 3 + ctx.strokeStyle = 'hotpink' + ctx.font = '50px i-curly' + ctx.strokeText('@napi-rs/canvas', 50, 300) + t.snapshot(canvas.getContent().toString('utf8')) +}) diff --git a/__test__/svg-canvas.spec.ts.md b/__test__/svg-canvas.spec.ts.md new file mode 100644 index 00000000..5e07b92d --- /dev/null +++ b/__test__/svg-canvas.spec.ts.md @@ -0,0 +1,26 @@ +# Snapshot report for `__test__/svg-canvas.spec.ts` + +The actual snapshot is saved in `svg-canvas.spec.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## should be able to export path/arc/rect + +> Snapshot 1 + + `␊ + ␊ + ␊ + ␊ + ␊ + ` + +## should be able to export text + +> Snapshot 1 + + `␊ + ␊ + ␊ + ␊ + ` diff --git a/__test__/svg-canvas.spec.ts.snap b/__test__/svg-canvas.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..1e097bf4c86be98e96ea3c4113d431a8f2ba129d GIT binary patch literal 8390 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 VW>lNKQsECzM!(L>%^5v;@_(cQ%K88R literal 0 HcmV?d00001 diff --git a/example/export-svg.js b/example/export-svg.js new file mode 100644 index 00000000..4a1f45f8 --- /dev/null +++ b/example/export-svg.js @@ -0,0 +1,34 @@ +const { readFileSync, writeFileSync } = require('fs') +const { join } = require('path') + +const { createCanvas, GlobalFonts, SvgExportFlag } = require('../index.js') + +const WoffFontPath = join(__dirname, '..', '__test__', 'fonts', 'Virgil.woff2') + +GlobalFonts.registerFromPath(WoffFontPath, 'Virgil') + +const canvas = createCanvas(1024, 768, SvgExportFlag.ConvertTextToPaths) +const ctx = canvas.getContext('2d') +ctx.fillStyle = 'yellow' +ctx.fillRect(0, 0, canvas.width, canvas.height) +ctx.strokeStyle = 'cyan' +ctx.lineWidth = 3 +ctx.font = '50px Virgil' +ctx.strokeText('skr canvas', 50, 150) + +ctx.strokeStyle = 'hotpink' + +ctx.strokeText('@napi-rs/canvas', 50, 300) + +ctx.strokeStyle = 'gray' + +ctx.strokeRect(50, 450, 100, 100) + +ctx.fillStyle = 'hotpink' + +ctx.arc(500, 120, 90, 0, Math.PI * 2) +ctx.fill() + +const b = canvas.getContent() + +writeFileSync(join(__dirname, 'export-text.svg'), b) diff --git a/example/export-text.svg b/example/export-text.svg new file mode 100644 index 00000000..cc6f4363 --- /dev/null +++ b/example/export-text.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/index.d.ts b/index.d.ts index 506d2305..b46269ef 100644 --- a/index.d.ts +++ b/index.d.ts @@ -273,6 +273,14 @@ export interface SKRSContext2D extends Omitstream = reinterpret_cast(w_stream); + c_surface->surface = reinterpret_cast(surface); + c_surface->canvas = reinterpret_cast(canvas.release()); + } + skiac_surface *skiac_surface_create_rgba_premultiplied(int width, int height) { return reinterpret_cast( @@ -156,7 +175,7 @@ extern "C" auto png_data = image->encodeToData().release(); if (png_data) { - data->ptr = const_cast(png_data->bytes()); + data->ptr = png_data->bytes(); data->size = png_data->size(); data->data = reinterpret_cast(png_data); } @@ -1072,7 +1091,7 @@ extern "C" void skiac_sk_data_destroy(skiac_data *c_data) { auto data = reinterpret_cast(c_data); - data->unref(); + SkSafeUnref(data); } // Bitmap @@ -1215,4 +1234,27 @@ extern "C" { delete c_font_collection; } + + // SkWStream + void skiac_sk_w_stream_get(skiac_w_memory_stream *c_w_memory_stream, skiac_sk_data *sk_data, int width, int height) + { + auto stream = reinterpret_cast(c_w_memory_stream); + stream->write("", 6); + auto data = stream->detachAsData().release(); + + sk_data->data = reinterpret_cast(data); + sk_data->ptr = data->bytes(); + sk_data->size = data->size(); + auto string = new SkString("\nappendS32(width); + string->append("\" height=\""); + string->appendS32(height); + string->append("\">\n"); + stream->write(string->c_str(), string->size()); + } + + void skiac_sk_w_stream_destroy(skiac_w_memory_stream *c_w_memory_stream) + { + delete reinterpret_cast(c_w_memory_stream); + } } diff --git a/skia-c/skia_c.hpp b/skia-c/skia_c.hpp index cba70b8f..76d570c5 100644 --- a/skia-c/skia_c.hpp +++ b/skia-c/skia_c.hpp @@ -53,6 +53,7 @@ typedef struct skiac_font_metrics skiac_font_metrics; typedef struct skiac_typeface skiac_typeface; typedef struct skiac_font_mgr skiac_font_mgr; typedef struct skiac_typeface_font_provider skiac_typeface_font_provider; +typedef struct skiac_w_memory_stream skiac_w_memory_stream; sk_sp SkFontMgr_New_Custom_Empty(); @@ -66,6 +67,13 @@ enum class CssBaseline Bottom, }; +struct skiac_svg_surface +{ + skiac_w_memory_stream *stream; + skiac_surface *surface; + skiac_canvas *canvas; +}; + struct skiac_font_collection { sk_sp collection; @@ -131,7 +139,7 @@ typedef void (*skiac_on_match_font_style)(int width, int weight, int slant, void struct skiac_sk_data { - uint8_t *ptr; + const uint8_t *ptr; size_t size; skiac_data *data; }; @@ -141,6 +149,7 @@ extern "C" // Surface skiac_surface *skiac_surface_create_rgba_premultiplied(int width, int height); + void skiac_surface_create_svg(skiac_svg_surface *c_surface, int width, int height, int alphaType, uint32_t flag); skiac_surface *skiac_surface_create_rgba(int width, int height); void skiac_surface_destroy(skiac_surface *c_surface); skiac_surface *skiac_surface_copy_rgba( @@ -359,6 +368,10 @@ extern "C" size_t skiac_font_collection_register(skiac_font_collection *c_font_collection, const uint8_t *font, size_t length, const char *name_alias); size_t skiac_font_collection_register_from_path(skiac_font_collection *c_font_collection, const char *font_path, const char *name_alias); void skiac_font_collection_destroy(skiac_font_collection *c_font_collection); + + // SkDynamicMemoryWStream + void skiac_sk_w_stream_get(skiac_w_memory_stream *c_w_memory_stream, skiac_sk_data *sk_data, int width, int height); + void skiac_sk_w_stream_destroy(skiac_w_memory_stream *c_w_memory_stream); } #endif // SKIA_CAPI_H diff --git a/src/ctx.rs b/src/ctx.rs index b7a2f0ff..f4941462 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,4 +1,4 @@ -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::f32::consts::PI; use std::mem; use std::rc::Rc; @@ -30,6 +30,9 @@ pub struct Context { pub alpha: bool, pub(crate) states: Vec, pub font_collection: Rc, + pub width: u32, + pub height: u32, + pub stream: Option, } impl Context { @@ -143,6 +146,29 @@ impl Context { ) } + #[inline(always)] + pub fn new_svg( + width: u32, + height: u32, + svg_export_flag: SvgExportFlag, + font_collection: &mut Rc, + ) -> Result { + let (surface, stream) = + Surface::new_svg(width, height, AlphaType::Unpremultiplied, svg_export_flag) + .ok_or_else(|| Error::from_reason("Create skia svg surface failed".to_owned()))?; + let states = vec![Context2dRenderingState::default()]; + Ok(Context { + surface, + alpha: true, + path: Path::new(), + states, + font_collection: font_collection.clone(), + width, + height, + stream: Some(stream), + }) + } + #[inline(always)] pub fn new(width: u32, height: u32, font_collection: &mut Rc) -> Result { let surface = Surface::new_rgba(width, height) @@ -154,6 +180,9 @@ impl Context { path: Path::new(), states, font_collection: font_collection.clone(), + width, + height, + stream: None, }) } @@ -572,7 +601,7 @@ impl Context { } } -#[js_function(3)] +#[js_function(4)] fn context_2d_constructor(ctx: CallContext) -> Result { let width: u32 = ctx.get::(0)?.try_into()?; let height: u32 = ctx.get::(1)?.try_into()?; @@ -580,7 +609,18 @@ fn context_2d_constructor(ctx: CallContext) -> Result { let font_collection = ctx.env.unwrap::>(&font_collection_js)?; let mut this = ctx.this_unchecked::(); - let context_2d = Context::new(width, height, font_collection)?; + let context_2d = if ctx.length == 3 { + Context::new(width, height, font_collection)? + } else { + // SVG Canvas + let flag = ctx.get::(3)?.get_uint32()?; + Context::new_svg( + width, + height, + SvgExportFlag::try_from(flag)?, + font_collection, + )? + }; ctx.env.wrap(&mut this, context_2d)?; ctx.env.get_undefined() } diff --git a/src/error.rs b/src/error.rs index 213f253e..8b3dfd26 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,8 @@ pub enum SkError { StringToStrokeCapError(String), #[error("[`{0}`] is not valid LineJoin value")] StringToStrokeJoinError(String), + #[error("[`{0}`] is not valid SvgExportFlag value")] + U32ToStrokeJoinError(u32), #[error("[`{0}`]")] Generic(String), } diff --git a/src/lib.rs b/src/lib.rs index 42ff0414..4d14d4c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,15 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> { ], )?; + let svg_canvas_element = env.define_class( + "SVGCanvas", + canvas_element_constructor, + &[ + Property::new(&env, "getContext")?.with_method(get_context), + Property::new(&env, "getContent")?.with_method(get_content), + ], + )?; + let canvas_rendering_context2d = ctx::Context::create_js_class(&env)?; let path_class = sk::Path::create_js_class(&env)?; @@ -78,6 +87,8 @@ fn init(mut exports: JsObject, env: Env) -> Result<()> { exports.set_named_property("CanvasElement", canvas_element)?; + exports.set_named_property("SVGCanvas", svg_canvas_element)?; + exports.set_named_property("Path2D", path_class)?; exports.set_named_property("ImageData", image_data_class)?; @@ -300,6 +311,24 @@ fn to_data_url_async(ctx: CallContext) -> Result { ctx.env.spawn(async_task).map(|p| p.promise_object()) } +#[js_function] +fn get_content(ctx: CallContext) -> Result { + let this = ctx.this_unchecked::(); + let ctx_js = this.get_named_property::("ctx")?; + let ctx2d = ctx.env.unwrap::(&ctx_js)?; + + let svg_data_stream = ctx2d.stream.as_ref().unwrap(); + let svg_data = svg_data_stream.data(ctx2d.width, ctx2d.height); + unsafe { + ctx + .env + .create_buffer_with_borrowed_data(svg_data.0.ptr, svg_data.0.size, svg_data, |d, _| { + mem::drop(d) + }) + .map(|b| b.into_raw()) + } +} + #[inline] fn get_data_ref(ctx: &CallContext, mime: &str, quality: u8) -> Result { let this = ctx.this_unchecked::(); diff --git a/src/sk.rs b/src/sk.rs index 16076f59..3cca0772 100644 --- a/src/sk.rs +++ b/src/sk.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::f32::consts::PI; use std::ffi::{c_void, CStr, CString}; use std::ops::{Deref, DerefMut}; @@ -22,6 +23,20 @@ mod ffi { _unused: [u8; 0], } + #[repr(C)] + #[derive(Copy, Clone, Debug)] + pub struct skiac_w_memory_stream { + _unused: [u8; 0], + } + + #[repr(C)] + #[derive(Copy, Clone, Debug)] + pub struct skiac_svg_surface { + pub stream: *mut skiac_w_memory_stream, + pub surface: *mut skiac_surface, + pub canvas: *mut skiac_canvas, + } + #[repr(C)] #[derive(Copy, Clone, Debug)] pub struct skiac_canvas { @@ -179,6 +194,14 @@ mod ffi { pub fn skiac_surface_create_rgba_premultiplied(width: i32, height: i32) -> *mut skiac_surface; + pub fn skiac_surface_create_svg( + c_surface: *mut skiac_svg_surface, + width: i32, + height: i32, + alphaType: i32, + flag: u32, + ); + pub fn skiac_surface_create_rgba(width: i32, height: i32) -> *mut skiac_surface; pub fn skiac_surface_destroy(surface: *mut skiac_surface); @@ -221,6 +244,16 @@ mod ffi { pub fn skiac_surface_get_alpha_type(surface: *mut skiac_surface) -> i32; + pub fn skiac_surface_draw_svg( + surface: *mut skiac_surface, + paint: *mut skiac_paint, + width: f32, + height: f32, + flag: u32, + sk_data: *mut skiac_sk_data, + ); + + // SkCanvas pub fn skiac_canvas_clear(canvas: *mut skiac_canvas, color: u32); pub fn skiac_canvas_set_transform(canvas: *mut skiac_canvas, ts: skiac_transform); @@ -641,6 +674,16 @@ mod ffi { ) -> usize; pub fn skiac_font_collection_destroy(c_font_collection: *mut skiac_font_collection); + + // SkDynamicMemoryStream + pub fn skiac_sk_w_stream_get( + c_w_memory_stream: *mut skiac_w_memory_stream, + sk_data: *mut skiac_sk_data, + w: i32, + h: i32, + ); + + pub fn skiac_sk_w_stream_destroy(c_w_memory_stream: *mut skiac_w_memory_stream); } } @@ -1264,6 +1307,27 @@ pub enum SkEncodedImageFormat { Avif, } +#[repr(u32)] +pub enum SvgExportFlag { + ConvertTextToPaths = 0x01, + NoPrettyXML = 0x02, + RelativePathEncoding = 0x04, +} + +impl TryFrom for SvgExportFlag { + type Error = SkError; + + #[inline] + fn try_from(value: u32) -> Result { + match value { + 0x01 => Ok(Self::ConvertTextToPaths), + 0x02 => Ok(Self::NoPrettyXML), + 0x04 => Ok(Self::RelativePathEncoding), + _ => Err(SkError::U32ToStrokeJoinError(value)), + } + } +} + pub struct Surface { ptr: *mut ffi::skiac_surface, pub(crate) canvas: Canvas, @@ -1285,6 +1349,39 @@ impl Surface { } } + #[inline] + pub fn new_svg( + width: u32, + height: u32, + alpha_type: AlphaType, + flag: SvgExportFlag, + ) -> Option<(Surface, SkWMemoryStream)> { + let mut svg_surface = ffi::skiac_svg_surface { + stream: ptr::null_mut(), + surface: ptr::null_mut(), + canvas: ptr::null_mut(), + }; + unsafe { + ffi::skiac_surface_create_svg( + &mut svg_surface, + width as i32, + height as i32, + alpha_type as i32, + flag as u32, + ); + }; + if svg_surface.surface.is_null() { + return None; + } + Some(( + Self { + ptr: svg_surface.surface, + canvas: Canvas(svg_surface.canvas), + }, + SkWMemoryStream(svg_surface.stream), + )) + } + #[inline] unsafe fn from_ptr(ptr: *mut ffi::skiac_surface) -> Option { if ptr.is_null() { @@ -1382,6 +1479,30 @@ impl Surface { } } + #[inline] + pub fn svg(&self, width: f32, height: f32, flag: SvgExportFlag) -> Option { + let mut data = ffi::skiac_sk_data { + ptr: ptr::null_mut(), + size: 0, + data: ptr::null_mut(), + }; + unsafe { + ffi::skiac_surface_draw_svg( + self.ptr, + ptr::null_mut(), + width, + height, + flag as u32, + &mut data, + ); + }; + if data.ptr.is_null() { + None + } else { + Some(SurfaceDataRef(data)) + } + } + #[inline] pub fn data_mut(&mut self) -> Option { unsafe { @@ -1484,6 +1605,30 @@ impl SurfaceRef { } } } + + #[inline] + pub fn svg(&self, width: f32, height: f32, flag: SvgExportFlag) -> Option { + let mut data = ffi::skiac_sk_data { + ptr: ptr::null_mut(), + size: 0, + data: ptr::null_mut(), + }; + unsafe { + ffi::skiac_surface_draw_svg( + self.0, + ptr::null_mut(), + width, + height, + flag as u32, + &mut data, + ); + }; + if data.ptr.is_null() { + None + } else { + Some(SurfaceDataRef(data)) + } + } } unsafe impl Send for SurfaceRef {} @@ -2024,6 +2169,9 @@ impl Drop for Paint { } } +unsafe impl Send for Paint {} +unsafe impl Sync for Paint {} + #[repr(transparent)] #[derive(Debug)] pub struct Path(pub(crate) *mut ffi::skiac_path); @@ -2982,6 +3130,29 @@ pub struct FontStyleSet { pub styles: Vec, } +#[derive(Debug, Clone)] +pub struct SkWMemoryStream(*mut ffi::skiac_w_memory_stream); + +impl SkWMemoryStream { + #[inline] + pub fn data(&self, w: u32, h: u32) -> SurfaceDataRef { + let mut data = ffi::skiac_sk_data { + ptr: ptr::null_mut(), + size: 0, + data: ptr::null_mut(), + }; + unsafe { ffi::skiac_sk_w_stream_get(self.0, &mut data, w as i32, h as i32) }; + SurfaceDataRef(data) + } +} + +impl Drop for SkWMemoryStream { + #[inline] + fn drop(&mut self) { + unsafe { ffi::skiac_sk_w_stream_destroy(self.0) } + } +} + #[inline(always)] fn radians_to_degrees(rad: f32) -> f32 { (rad / PI) * 180.0