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 00000000..1e097bf4 Binary files /dev/null and b/__test__/svg-canvas.spec.ts.snap differ 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