Skip to content

Commit

Permalink
feat: svg canvas backend
Browse files Browse the repository at this point in the history
  • Loading branch information
Brooooooklyn committed Jul 30, 2021
1 parent b77f45d commit f95f67a
Show file tree
Hide file tree
Showing 14 changed files with 444 additions and 15 deletions.
40 changes: 40 additions & 0 deletions __test__/svg-canvas.spec.ts
Original file line number Diff line number Diff line change
@@ -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'))
})
26 changes: 26 additions & 0 deletions __test__/svg-canvas.spec.ts.md

Large diffs are not rendered by default.

Binary file added __test__/svg-canvas.spec.ts.snap
Binary file not shown.
34 changes: 34 additions & 0 deletions example/export-svg.js
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions example/export-text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,14 @@ export interface SKRSContext2D extends Omit<CanvasRenderingContext2D, 'drawImage
}
}

export interface SvgCanvas {
width: number
height: number
getContext(contextType: '2d', contextAttributes?: { alpha: boolean }): SKRSContext2D

getContent(): Buffer
}

export interface Canvas {
width: number
height: number
Expand All @@ -296,6 +304,8 @@ export interface Canvas {

export function createCanvas(width: number, height: number): Canvas

export function createCanvas(width: number, height: number, svgExportFlag: SvgExportFlag): SvgCanvas

interface IGlobalFonts {
readonly families: {
family: string
Expand Down Expand Up @@ -341,3 +351,9 @@ export const enum StrokeCap {
Round = 1,
Square = 2,
}

export const enum SvgExportFlag {
ConvertTextToPaths = 0x01,
NoPrettyXML = 0x02,
RelativePathEncoding = 0x04,
}
23 changes: 15 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,8 @@ const { loadBinding } = require('@node-rs/helper')
* loadBinding helper will load `skia.[PLATFORM].node` from `__dirname` first
* If failed to load addon, it will fallback to load from `@napi-rs/skia-[PLATFORM]`
*/
const { CanvasRenderingContext2D, CanvasElement, Path2D, ImageData, Image, CanvasPattern, GlobalFonts } = loadBinding(
__dirname,
'skia',
'@napi-rs/canvas',
)
const { CanvasRenderingContext2D, CanvasElement, SVGCanvas, Path2D, ImageData, Image, CanvasPattern, GlobalFonts } =
loadBinding(__dirname, 'skia', '@napi-rs/canvas')

const Geometry = require('./geometry')

Expand Down Expand Up @@ -43,6 +40,12 @@ const FillType = {
InverseEvenOdd: 3,
}

const SvgExportFlag = {
ConvertTextToPaths: 0x01,
NoPrettyXML: 0x02,
RelativePathEncoding: 0x04,
}

const GlobalFontsSingleton = new GlobalFonts()
let FamilyNamesSet = JSON.parse(GlobalFontsSingleton._families)

Expand Down Expand Up @@ -140,9 +143,12 @@ Path2D.prototype.getFillTypeString = function getFillTypeString() {
}
}

function createCanvas(width, height) {
const canvasElement = new CanvasElement(width, height)
const ctx = new CanvasRenderingContext2D(width, height, GlobalFontsSingleton)
function createCanvas(width, height, flag) {
const isSvgBackend = typeof flag !== 'undefined'
const canvasElement = isSvgBackend ? new SVGCanvas(width, height) : new CanvasElement(width, height)
const ctx = isSvgBackend
? new CanvasRenderingContext2D(width, height, GlobalFontsSingleton, flag)
: new CanvasRenderingContext2D(width, height, GlobalFontsSingleton)

// napi can not define writable: true but enumerable: false property
Object.defineProperty(ctx, '_fillStyle', {
Expand Down Expand Up @@ -196,6 +202,7 @@ module.exports = {
FillType,
StrokeCap,
StrokeJoin,
SvgExportFlag,
...Geometry,
GlobalFonts: GlobalFontsSingleton,
}
2 changes: 1 addition & 1 deletion rust-toolchain
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nightly-2021-06-09
nightly-2021-07-29
46 changes: 44 additions & 2 deletions skia-c/skia_c.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ extern "C"
}
}

void skiac_surface_create_svg(skiac_svg_surface *c_surface, int w, int h, int alphaType, uint32_t flag)
{
auto w_stream = new SkDynamicMemoryWStream();

auto canvas = SkSVGCanvas::Make(SkRect::MakeWH(w, h), w_stream, flag);
if (!canvas.get())
{
return;
}
auto surface = skiac_surface_create(w, h, (SkAlphaType)alphaType);
if (!surface)
{
return;
}
c_surface->stream = reinterpret_cast<skiac_w_memory_stream *>(w_stream);
c_surface->surface = reinterpret_cast<skiac_surface *>(surface);
c_surface->canvas = reinterpret_cast<skiac_canvas *>(canvas.release());
}

skiac_surface *skiac_surface_create_rgba_premultiplied(int width, int height)
{
return reinterpret_cast<skiac_surface *>(
Expand Down Expand Up @@ -156,7 +175,7 @@ extern "C"
auto png_data = image->encodeToData().release();
if (png_data)
{
data->ptr = const_cast<uint8_t *>(png_data->bytes());
data->ptr = png_data->bytes();
data->size = png_data->size();
data->data = reinterpret_cast<skiac_data *>(png_data);
}
Expand Down Expand Up @@ -1072,7 +1091,7 @@ extern "C"
void skiac_sk_data_destroy(skiac_data *c_data)
{
auto data = reinterpret_cast<SkData *>(c_data);
data->unref();
SkSafeUnref(data);
}

// Bitmap
Expand Down Expand Up @@ -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<SkDynamicMemoryWStream *>(c_w_memory_stream);
stream->write("</svg>", 6);
auto data = stream->detachAsData().release();

sk_data->data = reinterpret_cast<skiac_data *>(data);
sk_data->ptr = data->bytes();
sk_data->size = data->size();
auto string = new SkString("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"");
string->appendS32(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<SkDynamicMemoryWStream *>(c_w_memory_stream);
}
}
15 changes: 14 additions & 1 deletion skia-c/skia_c.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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> SkFontMgr_New_Custom_Empty();

Expand All @@ -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<FontCollection> collection;
Expand Down Expand Up @@ -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;
};
Expand All @@ -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(
Expand Down Expand Up @@ -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
46 changes: 43 additions & 3 deletions src/ctx.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -30,6 +30,9 @@ pub struct Context {
pub alpha: bool,
pub(crate) states: Vec<Context2dRenderingState>,
pub font_collection: Rc<FontCollection>,
pub width: u32,
pub height: u32,
pub stream: Option<SkWMemoryStream>,
}

impl Context {
Expand Down Expand Up @@ -143,6 +146,29 @@ impl Context {
)
}

#[inline(always)]
pub fn new_svg(
width: u32,
height: u32,
svg_export_flag: SvgExportFlag,
font_collection: &mut Rc<FontCollection>,
) -> Result<Self> {
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<FontCollection>) -> Result<Self> {
let surface = Surface::new_rgba(width, height)
Expand All @@ -154,6 +180,9 @@ impl Context {
path: Path::new(),
states,
font_collection: font_collection.clone(),
width,
height,
stream: None,
})
}

Expand Down Expand Up @@ -572,15 +601,26 @@ impl Context {
}
}

#[js_function(3)]
#[js_function(4)]
fn context_2d_constructor(ctx: CallContext) -> Result<JsUndefined> {
let width: u32 = ctx.get::<JsNumber>(0)?.try_into()?;
let height: u32 = ctx.get::<JsNumber>(1)?.try_into()?;
let font_collection_js = ctx.get::<JsObject>(2)?;
let font_collection = ctx.env.unwrap::<Rc<FontCollection>>(&font_collection_js)?;

let mut this = ctx.this_unchecked::<JsObject>();
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::<JsNumber>(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()
}
Expand Down
2 changes: 2 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Loading

1 comment on commit f95f67a

@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: f95f67a Previous: b77f45d Ratio
Draw house#skia-canvas 20.6 ops/sec (±0.8%) 19.3 ops/sec (±0.95%) 0.94
Draw house#node-canvas 20.7 ops/sec (±0.93%) 20.1 ops/sec (±1.75%) 0.97
Draw house#@napi-rs/skia 19.8 ops/sec (±0.6%) 20.4 ops/sec (±1.5%) 1.03
Draw gradient#skia-canvas 19 ops/sec (±2.41%) 18 ops/sec (±1.76%) 0.95
Draw gradient#node-canvas 20.1 ops/sec (±0.76%) 19 ops/sec (±1.53%) 0.95
Draw gradient#@napi-rs/skia 19.1 ops/sec (±0.98%) 20 ops/sec (±1.22%) 1.05

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

Please sign in to comment.