diff --git a/__test__/image-data.spec.ts b/__test__/image-data.spec.ts index 98ae7261..958de7d3 100644 --- a/__test__/image-data.spec.ts +++ b/__test__/image-data.spec.ts @@ -40,7 +40,6 @@ test('properties should be readonly', (t) => { const fakeData = new Uint8ClampedArray() const expectation = { instanceOf: TypeError, - message: /Cannot assign to read only property/, } // @ts-expect-error diff --git a/index.js b/index.js index 17804dd7..5291f01d 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,6 @@ const { clearAllCache, CanvasRenderingContext2D, CanvasElement, - createContext, SVGCanvas, Path2D, ImageData, @@ -47,84 +46,9 @@ Object.defineProperty(GlobalFonts, 'has', { writable: false, }) -CanvasRenderingContext2D.prototype.createPattern = function createPattern(image, repetition) { - if (image instanceof ImageData) { - const pattern = new CanvasPattern(image, repetition, 0) - Object.defineProperty(pattern, '_imageData', { - writable: true, - configurable: false, - enumerable: false, - value: null, - }) - return pattern - } else if (image instanceof Image) { - return new CanvasPattern(image, repetition, 1) - } else if (image instanceof CanvasElement || image instanceof SVGCanvas) { - return new CanvasPattern(image, repetition, 2) - } - throw TypeError('Image should be instance of ImageData or Image') -} - -CanvasRenderingContext2D.prototype.getImageData = function getImageData(x, y, w, h) { - const data = this._getImageData(x, y, w, h) - return new ImageData(data, w, h) -} - function createCanvas(width, height, flag) { const isSvgBackend = typeof flag !== 'undefined' - const canvasElement = isSvgBackend ? new SVGCanvas(width, height) : new CanvasElement(width, height) - - let ctx - canvasElement.getContext = function getContext(type, attr = {}) { - if (type !== '2d') { - throw new Error('Unsupported type') - } - const attrs = { alpha: true, colorSpace: 'srgb', ...attr } - ctx = ctx - ? ctx - : isSvgBackend - ? new CanvasRenderingContext2D(this.width, this.height, attrs.colorSpace, flag) - : new CanvasRenderingContext2D(this.width, this.height, attrs.colorSpace) - createContext(ctx, this.width, this.height, attrs) - - // napi can not define writable: true but enumerable: false property - Object.defineProperty(ctx, '_fillStyle', { - value: '#000', - configurable: false, - enumerable: false, - writable: true, - }) - - Object.defineProperty(ctx, '_strokeStyle', { - value: '#000', - configurable: false, - enumerable: false, - writable: true, - }) - - Object.defineProperty(ctx, 'createImageData', { - value: function createImageData(widthOrImage, height, attrs = {}) { - if (widthOrImage instanceof ImageData) { - return new ImageData(widthOrImage.data, widthOrImage.width, widthOrImage.height) - } - return new ImageData(widthOrImage, height, { colorSpace: 'srgb', ...attrs }) - }, - configurable: true, - enumerable: false, - writable: true, - }) - - Object.defineProperty(canvasElement, 'ctx', { - value: ctx, - enumerable: false, - configurable: false, - }) - - ctx.canvas = canvasElement - - return ctx - } - + const canvasElement = isSvgBackend ? new SVGCanvas(width, height, flag) : new CanvasElement(width, height) const { encode: canvasEncode, encodeSync: canvasEncodeSync, diff --git a/js-binding.js b/js-binding.js index 4256ead5..79b8aa4f 100644 --- a/js-binding.js +++ b/js-binding.js @@ -199,7 +199,6 @@ const { clearAllCache, CanvasRenderingContext2D, CanvasElement, - createContext, SVGCanvas, Path, ImageData, @@ -216,7 +215,6 @@ const { module.exports.clearAllCache = clearAllCache module.exports.CanvasRenderingContext2D = CanvasRenderingContext2D module.exports.CanvasElement = CanvasElement -module.exports.createContext = createContext module.exports.SVGCanvas = SVGCanvas module.exports.Path2D = Path module.exports.ImageData = ImageData diff --git a/load-image.js b/load-image.js index 35c80556..8ff84fb1 100644 --- a/load-image.js +++ b/load-image.js @@ -39,7 +39,13 @@ module.exports = async function loadImage(source, options = {}) { source = !(source instanceof URL) ? new URL(source) : source // attempt to download the remote source and construct image const data = await new Promise((resolve, reject) => - makeRequest(source, resolve, reject, options.maxRedirects ?? MAX_REDIRECTS, options.requestOptions), + makeRequest( + source, + resolve, + reject, + typeof options.maxRedirects === 'number' && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS, + options.requestOptions, + ), ) return createImage(data, options.alt) } @@ -54,7 +60,7 @@ function makeRequest(url, resolve, reject, redirectCount, requestOptions) { // lazy load the lib const lib = isHttps ? (!https ? (https = require('https')) : https) : !http ? (http = require('http')) : http - lib.get(url, requestOptions ?? {}, (res) => { + lib.get(url, requestOptions || {}, (res) => { const shouldRedirect = REDIRECT_STATUSES.has(res.statusCode) && typeof res.headers.location === 'string' if (shouldRedirect && redirectCount > 0) return makeRequest(new URL(res.headers.location), resolve, reject, redirectCount - 1, requestOptions) diff --git a/src/ctx.rs b/src/ctx.rs index df0c40e2..47ca6646 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -1,4 +1,3 @@ -use std::convert::TryFrom; use std::f32::consts::PI; use std::mem; use std::result; @@ -6,17 +5,28 @@ use std::slice; use std::str::FromStr; use cssparser::{Color as CSSColor, Parser, ParserInput}; -use napi::bindgen_prelude::{ClassInstance, Either3, Unknown}; +use napi::bindgen_prelude::*; use napi::*; use rgb::FromSlice; -use crate::filter::css_filters_to_image_filter; -use crate::util::UnwrapObject; -use crate::CanvasElement; -use crate::SVGCanvas; +use crate::pattern::CanvasPattern; +use crate::sk::Transform; use crate::{ - error::SkError, filter::css_filter, font::Font, gradient::CanvasGradient, image::*, - pattern::Pattern, sk::*, state::Context2dRenderingState, + error::SkError, + filter::css_filter, + filter::css_filters_to_image_filter, + font::Font, + gradient::{CanvasGradient, Gradient}, + image::*, + path::Path, + pattern::Pattern, + sk::{ + AlphaType, Bitmap, BlendMode, ColorSpace, FillType, ImageFilter, LineMetrics, MaskFilter, + Matrix, Paint, PaintStyle, Path as SkPath, PathEffect, SkEncodedImageFormat, SkWMemoryStream, + SkiaDataRef, Surface, SurfaceRef, + }, + state::Context2dRenderingState, + CanvasElement, SVGCanvas, }; impl From for Error { @@ -25,26 +35,13 @@ impl From for Error { } } -const MAX_TEXT_WIDTH: f32 = 100_000.0; - -pub(crate) enum ImageOrCanvas { - Image(Image), - Canvas, -} - -impl ImageOrCanvas { - pub(crate) fn get_image(&mut self) -> Option<&mut Image> { - match self { - Self::Image(i) => Some(i), - _ => None, - } - } -} +pub(crate) const MAX_TEXT_WIDTH: f32 = 100_000.0; +pub(crate) const FILL_STYLE_HIDDEN_NAME: &str = "_fillStyle"; +pub(crate) const STROKE_STYLE_HIDDEN_NAME: &str = "_strokeStyle"; pub struct Context { - env: Env, pub(crate) surface: Surface, - path: Path, + path: SkPath, pub alpha: bool, pub(crate) states: Vec, state: Context2dRenderingState, @@ -54,225 +51,25 @@ pub struct Context { pub stream: Option, } -impl Drop for Context { - fn drop(&mut self) { - if let Err(e) = self - .env - .adjust_external_memory(-((self.width * self.height * 4) as i64)) - { - eprintln!("{e}"); - } - } -} - impl Context { - pub fn create_js_class(env: &Env) -> Result { - env.define_class( - "CanvasRenderingContext2D", - context_2d_constructor, - &vec![ - // properties - Property::new("miterLimit")? - .with_getter(get_miter_limit) - .with_setter(set_miter_limit), - Property::new("globalAlpha")? - .with_getter(get_global_alpha) - .with_setter(set_global_alpha), - Property::new("globalCompositeOperation")? - .with_getter(get_global_composite_operation) - .with_setter(set_global_composite_operation), - Property::new("imageSmoothingEnabled")? - .with_getter(get_image_smoothing_enabled) - .with_setter(set_image_smoothing_enabled), - Property::new("imageSmoothingQuality")? - .with_getter(get_image_smoothing_quality) - .with_setter(set_image_smoothing_quality), - Property::new("lineCap")? - .with_setter(set_line_cap) - .with_getter(get_line_cap), - Property::new("lineDashOffset")? - .with_setter(set_line_dash_offset) - .with_getter(get_line_dash_offset), - Property::new("lineJoin")? - .with_setter(set_line_join) - .with_getter(get_line_join), - Property::new("lineWidth")? - .with_setter(set_line_width) - .with_getter(get_line_width), - Property::new("fillStyle")? - .with_setter(set_fill_style) - .with_getter(get_fill_style), - Property::new("filter")? - .with_setter(set_filter) - .with_getter(get_filter), - Property::new("font")? - .with_setter(set_font) - .with_getter(get_font), - Property::new("direction")? - .with_setter(set_text_direction) - .with_getter(get_text_direction), - Property::new("strokeStyle")? - .with_setter(set_stroke_style) - .with_getter(get_stroke_style), - Property::new("shadowBlur")? - .with_setter(set_shadow_blur) - .with_getter(get_shadow_blur), - Property::new("shadowColor")? - .with_setter(set_shadow_color) - .with_getter(get_shadow_color), - Property::new("shadowOffsetX")? - .with_setter(set_shadow_offset_x) - .with_getter(get_shadow_offset_x), - Property::new("shadowOffsetY")? - .with_setter(set_shadow_offset_y) - .with_getter(get_shadow_offset_y), - Property::new("textAlign")? - .with_setter(set_text_align) - .with_getter(get_text_align), - Property::new("textBaseline")? - .with_setter(set_text_baseline) - .with_getter(get_text_baseline), - // methods - Property::new("arc")? - .with_method(arc) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("arcTo")? - .with_method(arc_to) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("beginPath")? - .with_method(begin_path) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("bezierCurveTo")? - .with_method(bezier_curve_to) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("clearRect")? - .with_method(clear_rect) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("clip")? - .with_method(clip) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("closePath")? - .with_method(close_path) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("createLinearGradient")? - .with_method(create_linear_gradient) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("createRadialGradient")? - .with_method(create_radial_gradient) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("createConicGradient")? - .with_method(create_conic_gradient) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("drawImage")? - .with_method(draw_image) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("getContextAttributes")? - .with_method(get_context_attributes) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("isPointInPath")? - .with_method(is_point_in_path) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("isPointInStroke")?.with_method(is_point_in_stroke), - Property::new("ellipse")? - .with_method(ellipse) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("lineTo")? - .with_method(line_to) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("measureText")? - .with_method(measure_text) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("moveTo")? - .with_method(move_to) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("fill")? - .with_method(fill) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("fillRect")? - .with_method(fill_rect) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("fillText")? - .with_method(fill_text) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("_getImageData")? - .with_method(get_image_data) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("getLineDash")? - .with_method(get_line_dash) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("putImageData")? - .with_method(put_image_data) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("quadraticCurveTo")? - .with_method(quadratic_curve_to) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("rect")? - .with_method(rect) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("resetTransform")? - .with_method(reset_transform) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("restore")? - .with_method(restore) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("rotate")? - .with_method(rotate) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("save")? - .with_method(save) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("scale")? - .with_method(scale) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("setLineDash")? - .with_method(set_line_dash) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("stroke")? - .with_method(stroke) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("strokeRect")? - .with_method(stroke_rect) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("strokeText")? - .with_method(stroke_text) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("translate")? - .with_method(translate) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("transform")? - .with_method(transform) - .with_property_attributes(PropertyAttributes::Writable), - // getter setter method - Property::new("getTransform")? - .with_method(get_current_transform) - .with_property_attributes(PropertyAttributes::Writable), - Property::new("setTransform")? - .with_method(set_current_transform) - .with_property_attributes(PropertyAttributes::Writable), - ], - ) - } - pub fn new_svg( - env: Env, width: u32, height: u32, - svg_export_flag: SvgExportFlag, + svg_export_flag: crate::sk::SvgExportFlag, color_space: ColorSpace, ) -> Result { let (surface, stream) = Surface::new_svg( width, height, - AlphaType::Unpremultiplied, + AlphaType::Premultiplied, svg_export_flag, color_space, ) .ok_or_else(|| Error::from_reason("Create skia svg surface failed".to_owned()))?; Ok(Context { - env, surface, alpha: true, - path: Path::new(), + path: SkPath::new(), states: vec![], state: Context2dRenderingState::default(), width, @@ -282,14 +79,13 @@ impl Context { }) } - pub fn new(env: Env, width: u32, height: u32, color_space: ColorSpace) -> Result { + pub fn new(width: u32, height: u32, color_space: ColorSpace) -> Result { let surface = Surface::new_rgba_premultiplied(width, height, color_space) .ok_or_else(|| Error::from_reason("Create skia surface failed".to_owned()))?; Ok(Context { - env, surface, alpha: true, - path: Path::new(), + path: SkPath::new(), states: vec![], state: Context2dRenderingState::default(), width, @@ -313,6 +109,10 @@ impl Context { .arc(center_x, center_y, radius, start_angle, end_angle, from_end); } + pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) { + self.path.arc_to_tangent(x1, y1, x2, y2, radius); + } + pub fn ellipse( &mut self, x: f32, @@ -337,11 +137,19 @@ impl Context { } pub fn begin_path(&mut self) { - let mut new_sub_path = Path::new(); + let mut new_sub_path = SkPath::new(); self.path.swap(&mut new_sub_path); } - pub fn clip(&mut self, path: Option<&mut Path>, fill_rule: FillType) { + pub fn bezier_curve_to(&mut self, cp1x: f32, cp1y: f32, cp2x: f32, cp2y: f32, x: f32, y: f32) { + self.path.cubic_to(cp1x, cp1y, cp2x, cp2y, x, y); + } + + pub fn quadratic_curve_to(&mut self, cpx: f32, cpy: f32, x: f32, y: f32) { + self.path.quad_to(cpx, cpy, x, y); + } + + pub fn clip(&mut self, path: Option<&mut SkPath>, fill_rule: FillType) { let clip = match path { Some(path) => path, None => &mut self.path, @@ -350,6 +158,23 @@ impl Context { self.surface.canvas.set_clip_path(clip); } + pub fn clear_rect(&mut self, x: f32, y: f32, width: f32, height: f32) { + let mut paint = Paint::new(); + paint.set_style(PaintStyle::Fill); + paint.set_color(0, 0, 0, 0); + paint.set_stroke_miter(10.0); + paint.set_blend_mode(BlendMode::Clear); + self.surface.draw_rect(x, y, width, height, &paint); + } + + pub fn close_path(&mut self) { + self.path.close(); + } + + pub fn rect(&mut self, x: f32, y: f32, width: f32, height: f32) { + self.path.add_rect(x, y, width, height); + } + pub fn save(&mut self) { self.surface.canvas.save(); self.states.push(self.state.clone()); @@ -488,7 +313,7 @@ impl Context { Ok(()) } - pub fn stroke(&mut self, path: Option<&mut Path>) -> Result<()> { + pub fn stroke(&mut self, path: Option<&mut SkPath>) -> Result<()> { let last_state = &self.state; let p = match path { Some(path) => path, @@ -513,7 +338,7 @@ impl Context { pub fn fill( &mut self, - path: Option<&mut Path>, + path: Option<&mut SkPath>, fill_rule: FillType, ) -> result::Result<(), SkError> { let last_state = &self.state; @@ -593,6 +418,80 @@ impl Context { Ok(()) } + pub fn get_font(&self) -> &str { + &self.state.font + } + + pub fn set_font(&mut self, font: String) -> result::Result<(), SkError> { + self.state.font_style = Font::new(&font)?; + self.state.font = font; + Ok(()) + } + + pub fn get_stroke_width(&self) -> f32 { + self.state.paint.get_stroke_width() + } + + pub fn get_miter_limit(&self) -> f32 { + self.state.paint.get_stroke_miter() + } + + pub fn set_miter_limit(&mut self, miter_limit: f32) { + self.state.paint.set_stroke_miter(miter_limit); + } + + pub fn get_global_alpha(&self) -> f64 { + self.state.paint.get_alpha() as f64 / 255.0 + } + + pub fn set_shadow_color(&mut self, shadow_color: String) -> result::Result<(), SkError> { + let mut parser_input = ParserInput::new(&shadow_color); + let mut parser = Parser::new(&mut parser_input); + let color = CSSColor::parse(&mut parser) + .map_err(|e| SkError::Generic(format!("Parse color [{}] error: {:?}", &shadow_color, e)))?; + + match color { + CSSColor::CurrentColor => { + return Err(SkError::Generic( + "Color should not be `currentcolor` keyword".to_owned(), + )) + } + CSSColor::RGBA(rgba) => { + drop(parser_input); + self.state.shadow_color_string = shadow_color; + self.state.shadow_color = rgba; + } + } + Ok(()) + } + + pub fn set_text_align(&mut self, text_align: String) -> result::Result<(), SkError> { + self.state.text_align = text_align.parse()?; + Ok(()) + } + + pub fn set_text_baseline(&mut self, text_baseline: String) -> result::Result<(), SkError> { + self.state.text_baseline = text_baseline.parse()?; + Ok(()) + } + + pub fn get_image_data( + &mut self, + x: f32, + y: f32, + w: f32, + h: f32, + color_type: ColorSpace, + ) -> Option> { + self + .surface + .read_pixels(x as u32, y as u32, w as u32, h as u32, color_type) + } + + pub fn set_line_dash(&mut self, line_dash_list: Vec) { + self.state.line_dash_list = line_dash_list; + } + fn stroke_paint(&self) -> result::Result { let last_state = &self.state; let current_paint = &last_state.paint; @@ -851,1436 +750,1140 @@ impl Context { } } -#[js_function(4)] -fn context_2d_constructor(ctx: CallContext) -> Result { - let width = ctx.get::(0)?.get_uint32()?; - let height = ctx.get::(1)?.get_uint32()?; - let color_space = ctx.get::(2)?.into_utf8()?; - let color_space = ColorSpace::from_str(color_space.as_str()?)?; - - let mut this = ctx.this_unchecked::(); - let context_2d = if ctx.length == 3 { - Context::new(*ctx.env, width, height, color_space)? - } else { - // SVG Canvas - let flag = ctx.get::(3)?.get_uint32()?; - Context::new_svg( - *ctx.env, - width, - height, - SvgExportFlag::try_from(flag)?, - color_space, - )? - }; - ctx - .env - .adjust_external_memory((width * height * 4) as i64)?; - ctx.env.wrap(&mut this, context_2d)?; - ctx.env.get_undefined() +#[napi(object)] +pub struct ContextAttributes { + pub alpha: bool, + pub desynchronized: bool, } -#[js_function(6)] -fn arc(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let center_x = ctx.get::(0)?.get_double()? as f32; - let center_y = ctx.get::(1)?.get_double()? as f32; - let radius = ctx.get::(2)?.get_double()? as f32; - let start_angle = ctx.get::(3)?.get_double()? as f32; - let end_angle = ctx.get::(4)?.get_double()? as f32; - let from_end = ctx - .get::(5) - .and_then(|js_bool| js_bool.get_value()) - .unwrap_or(false); - context_2d.arc(center_x, center_y, radius, start_angle, end_angle, from_end); - ctx.env.get_undefined() +#[napi] +pub enum SvgExportFlag { + ConvertTextToPaths = 0x01, + NoPrettyXML = 0x02, + RelativePathEncoding = 0x04, } -#[js_function(5)] -fn arc_to(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let ctrl_x = ctx.get::(0)?.get_double()? as f32; - let ctrl_y = ctx.get::(1)?.get_double()? as f32; - let to_x = ctx.get::(2)?.get_double()? as f32; - let to_y = ctx.get::(3)?.get_double()? as f32; - let radius = ctx.get::(4)?.get_double()? as f32; +impl From for crate::sk::SvgExportFlag { + fn from(value: SvgExportFlag) -> Self { + match value { + SvgExportFlag::ConvertTextToPaths => crate::sk::SvgExportFlag::ConvertTextToPaths, + SvgExportFlag::NoPrettyXML => crate::sk::SvgExportFlag::NoPrettyXML, + SvgExportFlag::RelativePathEncoding => crate::sk::SvgExportFlag::RelativePathEncoding, + } + } +} - context_2d - .path - .arc_to_tangent(ctrl_x, ctrl_y, to_x, to_y, radius); - ctx.env.get_undefined() +#[napi(custom_finalize)] +pub struct CanvasRenderingContext2D { + pub(crate) context: Context, } -#[js_function] -fn begin_path(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.begin_path(); - ctx.env.get_undefined() +impl ObjectFinalize for CanvasRenderingContext2D { + fn finalize(self, mut env: Env) -> Result<()> { + env.adjust_external_memory(-((self.context.width * self.context.height * 4) as i64))?; + Ok(()) + } } -#[js_function(6)] -fn bezier_curve_to(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; +#[napi] +impl CanvasRenderingContext2D { + #[napi(constructor)] + pub fn new( + width: u32, + height: u32, + color_space: String, + flag: Option, + ) -> Result { + let color_space = ColorSpace::from_str(&color_space)?; + let context = if let Some(flag) = flag { + Context::new_svg(width, height, flag.into(), color_space)? + } else { + Context::new(width, height, color_space)? + }; + Ok(Self { context }) + } - let cp1x = ctx.get::(0)?.get_double()? as f32; - let cp1y = ctx.get::(1)?.get_double()? as f32; - let cp2x = ctx.get::(2)?.get_double()? as f32; - let cp2y = ctx.get::(3)?.get_double()? as f32; - let x = ctx.get::(4)?.get_double()? as f32; - let y = ctx.get::(5)?.get_double()? as f32; + #[napi(getter)] + pub fn get_miter_limit(&self) -> f32 { + self.context.get_miter_limit() + } - context_2d.path.cubic_to(cp1x, cp1y, cp2x, cp2y, x, y); + #[napi(setter, return_if_invalid)] + pub fn set_miter_limit(&mut self, miter_limit: f64) { + if !miter_limit.is_nan() && !miter_limit.is_infinite() { + self.context.set_miter_limit(miter_limit as f32); + } + } - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_global_alpha(&self) -> f64 { + self.context.get_global_alpha() + } -#[js_function(4)] -fn quadratic_curve_to(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(setter, return_if_invalid)] + pub fn set_global_alpha(&mut self, alpha: f64) { + let alpha = alpha as f32; + if !(0.0..=1.0).contains(&alpha) { + #[cfg(debug_assertions)] + eprintln!( + "Alpha value out of range, expected 0.0 - 1.0, but got : {}", + alpha + ); + return; + } - let cpx = ctx.get::(0)?.get_double()? as f32; - let cpy = ctx.get::(1)?.get_double()? as f32; - let x = ctx.get::(2)?.get_double()? as f32; - let y = ctx.get::(3)?.get_double()? as f32; + self.context.state.paint.set_alpha((alpha * 255.0) as u8); + } - context_2d.path.quad_to(cpx, cpy, x, y); + #[napi(getter)] + pub fn get_global_composite_operation(&self) -> String { + self + .context + .state + .paint + .get_blend_mode() + .as_str() + .to_owned() + } - ctx.env.get_undefined() -} + #[napi(setter, return_if_invalid)] + pub fn set_global_composite_operation(&mut self, mode: String) { + if let Ok(blend_mode) = mode.parse() { + self.context.state.paint.set_blend_mode(blend_mode); + }; + } -#[js_function(2)] -fn clip(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - if ctx.length == 0 { - context_2d.clip(None, FillType::Winding); - } else if ctx.length == 1 { - let rule = ctx.get::(0)?; - context_2d.clip(None, FillType::from_str(rule.into_utf8()?.as_str()?)?); - } else { - let path = ctx.get::(0)?; - let rule = ctx.get::(1)?; - context_2d.clip( - Some(&mut path.unwrap::(ctx.env)?.inner), - FillType::from_str(rule.into_utf8()?.as_str()?)?, - ); - }; + #[napi(getter)] + pub fn get_image_smoothing_enabled(&self) -> bool { + self.context.state.image_smoothing_enabled + } - ctx.env.get_undefined() -} + #[napi(setter, return_if_invalid)] + pub fn set_image_smoothing_enabled(&mut self, enabled: bool) { + self.context.state.image_smoothing_enabled = enabled; + } -#[js_function(4)] -fn rect(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let width = ctx.get::(2)?.get_double()? as f32; - let height = ctx.get::(3)?.get_double()? as f32; - - context_2d - .path - .add_rect(x as f32, y as f32, width as f32, height as f32); - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_image_smoothing_quality(&self) -> String { + self + .context + .state + .image_smoothing_quality + .as_str() + .to_owned() + } -#[js_function(2)] -fn fill(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - if ctx.length == 0 { - context_2d.fill(None, FillType::Winding)?; - } else if ctx.length == 1 { - let input = ctx.get::(0)?; - match input.get_type()? { - ValueType::String => { - let fill_rule_js = unsafe { input.cast::() }.into_utf8()?; - context_2d.fill(None, FillType::from_str(fill_rule_js.as_str()?)?)?; - } - ValueType::Object => { - let path_js = ctx.get::(0)?; - let path = &mut path_js.unwrap::(ctx.env)?.inner; - context_2d.fill(Some(path), FillType::Winding)?; - } - _ => { - return Err(Error::new( - Status::InvalidArg, - "Invalid fill argument".to_string(), - )) - } - } - } else { - let path_js = ctx.get::(0)?; - let fill_rule_js = ctx.get::(1)?.into_utf8()?; - let path = &mut path_js.unwrap::(ctx.env)?.inner; - context_2d.fill(Some(path), FillType::from_str(fill_rule_js.as_str()?)?)?; - }; + #[napi(setter, return_if_invalid)] + pub fn set_image_smoothing_quality(&mut self, quality: String) { + if let Ok(quality) = quality.parse() { + self.context.state.image_smoothing_quality = quality; + }; + } - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_line_cap(&self) -> String { + self + .context + .state + .paint + .get_stroke_cap() + .as_str() + .to_owned() + } -#[js_function] -fn save(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.save(); - ctx.env.get_undefined() -} + #[napi(setter, return_if_invalid)] + pub fn set_line_cap(&mut self, cap: String) { + if let Ok(cap) = cap.parse() { + self.context.state.paint.set_stroke_cap(cap); + }; + } -#[js_function] -fn restore(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.restore(); - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_line_dash_offset(&self) -> f64 { + self.context.state.line_dash_offset as f64 + } -#[js_function(1)] -fn rotate(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(setter, return_if_invalid)] + pub fn set_line_dash_offset(&mut self, offset: f64) { + self.context.state.line_dash_offset = offset as f32; + } - let angle = ctx.get::(0)?.get_double()? as f32; - context_2d.rotate(angle); - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_line_join(&self) -> String { + self + .context + .state + .paint + .get_stroke_join() + .as_str() + .to_owned() + } -#[js_function(4)] -fn clear_rect(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let width = ctx.get::(2)?.get_double()? as f32; - let height = ctx.get::(3)?.get_double()? as f32; - - let mut paint = Paint::new(); - paint.set_style(PaintStyle::Fill); - paint.set_color(0, 0, 0, 0); - paint.set_stroke_miter(10.0); - paint.set_blend_mode(BlendMode::Clear); - context_2d.surface.draw_rect(x, y, width, height, &paint); - ctx.env.get_undefined() -} + #[napi(setter, return_if_invalid)] + pub fn set_line_join(&mut self, join: String) { + if let Ok(join) = join.parse() { + self.context.state.paint.set_stroke_join(join); + }; + } -#[js_function(4)] -fn create_linear_gradient(ctx: CallContext) -> Result> { - let x0 = ctx.get::(0)?.get_double()? as f32; - let y0 = ctx.get::(1)?.get_double()? as f32; - let x1 = ctx.get::(2)?.get_double()? as f32; - let y1 = ctx.get::(3)?.get_double()? as f32; - let linear_gradient = CanvasGradient::create_linear_gradient(x0, y0, x1, y1); - crate::gradient::Gradient::into_instance(crate::gradient::Gradient(linear_gradient), *ctx.env) -} + #[napi(getter)] + pub fn get_line_width(&self) -> f64 { + self.context.state.paint.get_stroke_width() as f64 + } -#[js_function(6)] -fn create_radial_gradient(ctx: CallContext) -> Result> { - let x0 = ctx.get::(0)?.get_double()? as f32; - let y0 = ctx.get::(1)?.get_double()? as f32; - let r0 = ctx.get::(2)?.get_double()? as f32; - let x1 = ctx.get::(3)?.get_double()? as f32; - let y1 = ctx.get::(4)?.get_double()? as f32; - let r1 = ctx.get::(5)?.get_double()? as f32; - let radial_gradient = CanvasGradient::create_radial_gradient(x0, y0, r0, x1, y1, r1); - crate::gradient::Gradient::into_instance(crate::gradient::Gradient(radial_gradient), *ctx.env) -} + #[napi(setter, return_if_invalid)] + pub fn set_line_width(&mut self, width: f64) { + self.context.state.paint.set_stroke_width(width as f32); + } -#[js_function(3)] -fn create_conic_gradient(ctx: CallContext) -> Result> { - let r = ctx.get::(0)?.get_double()? as f32; - let x = ctx.get::(1)?.get_double()? as f32; - let y = ctx.get::(2)?.get_double()? as f32; - let conic_gradient = CanvasGradient::create_conic_gradient(x, y, r); - crate::gradient::Gradient::into_instance(crate::gradient::Gradient(conic_gradient), *ctx.env) -} + #[napi(getter)] + pub fn get_fill_style(&self, this: This) -> Result { + this.get_named_property_unchecked(FILL_STYLE_HIDDEN_NAME) + } -#[js_function] -fn close_path(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(setter, return_if_invalid)] + pub fn set_fill_style( + &mut self, + env: Env, + mut this: This, + fill_style: Either3, ClassInstance>, + ) -> Result<()> { + if let Some(pattern) = match &fill_style { + Either3::A(color) => Pattern::from_color(color.into_utf8()?.as_str()?).ok(), + Either3::B(gradient) => Some(Pattern::Gradient(gradient.0.clone())), + Either3::C(pattern) => Some(pattern.inner.clone()), + } { + let raw_fill_style = fill_style.as_unknown(env); + self.context.state.fill_style = pattern; + this.set(FILL_STYLE_HIDDEN_NAME, &raw_fill_style)?; + } + Ok(()) + } - context_2d.path.close(); - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_filter(&self) -> String { + self.context.state.filters_string.clone() + } -#[js_function(9)] -fn draw_image(ctx: CallContext) -> Result { - use napi::bindgen_prelude::ValidateNapiValue; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let image_js = ctx.get::(0)?; - let mut the_canvas = ImageOrCanvas::Canvas; - let image_or_canvas = if unsafe { - <&CanvasElement>::validate(ctx.env.raw(), image_js.raw()).is_ok() - || <&SVGCanvas>::validate(ctx.env.raw(), image_js.raw()).is_ok() - } { - &mut the_canvas - } else { - ctx.env.unwrap::(&image_js)? - }; - - match image_or_canvas { - ImageOrCanvas::Image(image) => { - if !image.complete { - return ctx.env.get_undefined(); - } - let data = image_js - .get_named_property_unchecked::("_src")? - .into_value()?; + #[napi(setter, return_if_invalid)] + pub fn set_filter(&mut self, filter: String) -> Result<()> { + self.context.set_filter(&filter)?; + Ok(()) + } - image.regenerate_bitmap_if_need(data); + #[napi(getter)] + pub fn get_font(&self) -> String { + self.context.get_font().to_owned() + } - // SVG with 0 width or 0 height - if image.bitmap.is_none() { - return ctx.env.get_undefined(); - } + #[napi(setter, return_if_invalid)] + pub fn set_font(&mut self, font: String) -> Result<()> { + self.context.set_font(font)?; + Ok(()) + } - let bitmap = image.bitmap.as_ref().unwrap(); - let image_w = bitmap.0.width as f32; - let image_h = bitmap.0.height as f32; - - if ctx.length == 3 { - let dx: f64 = ctx.get::(1)?.get_double()?; - let dy: f64 = ctx.get::(2)?.get_double()?; - context_2d.draw_image( - bitmap, 0f32, 0f32, image_w, image_h, dx as f32, dy as f32, image_w, image_h, - )?; - } else if ctx.length == 5 { - let dx: f64 = ctx.get::(1)?.get_double()?; - let dy: f64 = ctx.get::(2)?.get_double()?; - let d_width: f64 = ctx.get::(3)?.get_double()?; - let d_height: f64 = ctx.get::(4)?.get_double()?; - context_2d.draw_image( - bitmap, - 0f32, - 0f32, - image_w, - image_h, - dx as f32, - dy as f32, - d_width as f32, - d_height as f32, - )?; - } else if ctx.length == 9 { - let sx: f64 = ctx.get::(1)?.get_double()?; - let sy: f64 = ctx.get::(2)?.get_double()?; - let s_width: f64 = ctx.get::(3)?.get_double()?; - let s_height: f64 = ctx.get::(4)?.get_double()?; - let dx: f64 = ctx.get::(5)?.get_double()?; - let dy: f64 = ctx.get::(6)?.get_double()?; - let d_width: f64 = ctx.get::(7)?.get_double()?; - let d_height: f64 = ctx.get::(8)?.get_double()?; - context_2d.draw_image( - bitmap, - sx as f32, - sy as f32, - s_width as f32, - s_height as f32, - dx as f32, - dy as f32, - d_width as f32, - d_height as f32, - )?; - } + #[napi(getter)] + pub fn get_text_direction(&self) -> String { + self.context.state.text_direction.as_str().to_owned() + } - image.need_regenerate_bitmap = false; - } - ImageOrCanvas::Canvas => { - let ctx_js = image_js.get_named_property_unchecked::("ctx")?; - let another_ctx = ctx.env.unwrap::(&ctx_js)?; - let width = another_ctx.width as f32; - let height = another_ctx.height as f32; - let (sx, sy, sw, sh, dx, dy, dw, dh) = if ctx.length == 3 { - let dx = ctx.get::(1)?.get_double()? as f32; - let dy = ctx.get::(2)?.get_double()? as f32; - (0.0f32, 0.0f32, width, height, dx, dy, width, height) - } else if ctx.length == 5 { - let dx = ctx.get::(1)?.get_double()? as f32; - let dy = ctx.get::(2)?.get_double()? as f32; - let d_width = ctx.get::(3)?.get_double()? as f32; - let d_height = ctx.get::(4)?.get_double()? as f32; - (0.0, 0.0, width, height, dx, dy, d_width, d_height) - } else if ctx.length == 9 { - ( - ctx.get::(1)?.get_double()? as f32, - ctx.get::(2)?.get_double()? as f32, - ctx.get::(3)?.get_double()? as f32, - ctx.get::(4)?.get_double()? as f32, - ctx.get::(5)?.get_double()? as f32, - ctx.get::(6)?.get_double()? as f32, - ctx.get::(7)?.get_double()? as f32, - ctx.get::(8)?.get_double()? as f32, - ) - } else { - return Err(Error::new( - Status::InvalidArg, - format!("Invalid arguments length {}", ctx.length), - )); - }; + #[napi(setter, return_if_invalid)] + pub fn set_text_direction(&mut self, direction: String) { + if let Ok(d) = direction.parse() { + self.context.state.text_direction = d; + }; + } - context_2d.draw_image( - &another_ctx.surface.get_bitmap(), - sx, - sy, - sw, - sh, - dx, - dy, - dw, - dh, - )?; + #[napi(getter)] + pub fn get_stroke_style(&self, this: This) -> Option { + this.get(STROKE_STYLE_HIDDEN_NAME).ok().flatten() + } + + #[napi(setter, return_if_invalid)] + pub fn set_stroke_style( + &mut self, + env: Env, + mut this: This, + fill_style: Either3, ClassInstance>, + ) -> Result<()> { + if let Some(pattern) = match &fill_style { + Either3::A(color) => Pattern::from_color(color.into_utf8()?.as_str()?).ok(), + Either3::B(gradient) => Some(Pattern::Gradient(gradient.0.clone())), + Either3::C(pattern) => Some(pattern.inner.clone()), + } { + let raw_fill_style = fill_style.as_unknown(env); + this.set(STROKE_STYLE_HIDDEN_NAME, &raw_fill_style)?; + self.context.state.stroke_style = pattern; } - }; + Ok(()) + } - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_shadow_blur(&self) -> f64 { + self.context.state.shadow_blur as f64 + } -#[js_function] -fn get_context_attributes(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let mut obj = ctx.env.create_object()?; - obj.set_named_property("alpha", ctx.env.get_boolean(context_2d.alpha)?)?; - obj.set_named_property("desynchronized", ctx.env.get_boolean(false)?)?; - Ok(obj) -} + #[napi(setter, return_if_invalid)] + pub fn set_shadow_blur(&mut self, blur: f64) { + self.context.state.shadow_blur = blur as f32; + } -#[js_function(4)] -fn is_point_in_path(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let result; - - if ctx.length == 2 { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - result = context_2d.path.hit_test(y, x, FillType::Winding); - ctx.env.get_boolean(result) - } else if ctx.length == 3 { - let input = ctx.get::(0)?; - match input.get_type()? { - ValueType::Number => { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let fill_rule_js = ctx.get::(2)?.into_utf8()?; - result = context_2d - .path - .hit_test(y, x, FillType::from_str(fill_rule_js.as_str()?)?); - } - ValueType::Object => { - let x = ctx.get::(1)?.get_double()? as f32; - let y = ctx.get::(2)?.get_double()? as f32; - let path_js = ctx.get::(0)?; - let path = &mut path_js.unwrap::(ctx.env)?.inner; - result = path.hit_test(x, y, FillType::Winding); - } - _ => { - return Err(Error::new( - Status::InvalidArg, - "Invalid isPointInPath argument".to_string(), - )) - } - } - ctx.env.get_boolean(result) - } else if ctx.length == 4 { - let path_js = ctx.get::(0)?; - let path = &mut path_js.unwrap::(ctx.env)?.inner; - let x = ctx.get::(1)?.get_double()? as f32; - let y = ctx.get::(2)?.get_double()? as f32; - let fill_rule_js = ctx.get::(3)?.into_utf8()?; - result = path.hit_test(y, x, FillType::from_str(fill_rule_js.as_str()?)?); - ctx.env.get_boolean(result) - } else { - Err(Error::new( - Status::InvalidArg, - "Invalid isPointInPath arguments length".to_string(), - )) + #[napi(getter)] + pub fn get_shadow_color(&self) -> String { + self.context.state.shadow_color_string.clone() } -} -#[js_function(3)] -fn is_point_in_stroke(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let mut result = false; - - if ctx.length == 2 { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let stroke_w = context_2d.state.paint.get_stroke_width(); - result = context_2d.path.stroke_hit_test(x, y, stroke_w); - } else if ctx.length == 3 { - let path_js = ctx.get::(0)?; - let path = &mut path_js.unwrap::(ctx.env)?.inner; - - let x = ctx.get::(1)?.get_double()? as f32; - let y = ctx.get::(2)?.get_double()? as f32; - let stroke_w = context_2d.state.paint.get_stroke_width(); - result = path.stroke_hit_test(x, y, stroke_w); - } - ctx.env.get_boolean(result) -} + #[napi(setter, return_if_invalid)] + pub fn set_shadow_color(&mut self, shadow_color: String) -> Result<()> { + self.context.set_shadow_color(shadow_color)?; + Ok(()) + } -#[js_function(8)] -fn ellipse(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let radius_x = ctx.get::(2)?.get_double()? as f32; - let radius_y = ctx.get::(3)?.get_double()? as f32; - let rotation = ctx.get::(4)?.get_double()? as f32; - let start_angle = ctx.get::(5)?.get_double()? as f32; - let end_angle = ctx.get::(6)?.get_double()? as f32; - - let from_end = if ctx.length == 8 { - ctx.get::(7)?.get_value()? - } else { - false - }; - context_2d.ellipse( - x, - y, - radius_x, - radius_y, - rotation, - start_angle, - end_angle, - from_end, - ); - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_shadow_offset_x(&self) -> f64 { + self.context.state.shadow_offset_x as f64 + } -#[js_function(2)] -fn line_to(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - if let Ok((x, y)) = ctx.get::(0)?.get_double().and_then(|x| { - ctx - .get::(1)? - .get_double() - .map(|y| (x as f32, y as f32)) - }) { - if !x.is_nan() && !y.is_nan() { - context_2d.path.line_to(x, y); - } + #[napi(setter, return_if_invalid)] + pub fn set_shadow_offset_x(&mut self, offset_x: f64) { + self.context.state.shadow_offset_x = offset_x as f32; } - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_shadow_offset_y(&self) -> f64 { + self.context.state.shadow_offset_y as f64 + } -#[js_function(1)] -fn measure_text(ctx: CallContext) -> Result { - let text = ctx.get::(0)?.into_utf8()?; - let this = ctx.this_unchecked::(); - let mut metrics = ctx.env.create_object()?; - let text_str = text.as_str()?; - if text_str.is_empty() { - metrics.set_named_property("actualBoundingBoxAscent", ctx.env.create_double(0.0f64)?)?; - metrics.set_named_property("actualBoundingBoxDescent", ctx.env.create_double(0.0f64)?)?; - metrics.set_named_property("actualBoundingBoxLeft", ctx.env.create_double(0.0f64)?)?; - metrics.set_named_property("actualBoundingBoxRight", ctx.env.create_double(0.0f64)?)?; - metrics.set_named_property("fontBoundingBoxAscent", ctx.env.create_double(0.0f64)?)?; - metrics.set_named_property("fontBoundingBoxDescent", ctx.env.create_double(0.0f64)?)?; - metrics.set_named_property("width", ctx.env.create_double(0.0f64)?)?; - return Ok(metrics); - } - - let context_2d = ctx.env.unwrap::(&this)?; - - let m = context_2d.get_line_metrics(text_str)?.0; - - metrics.set_named_property( - "actualBoundingBoxAscent", - ctx.env.create_double(m.ascent as f64)?, - )?; - metrics.set_named_property( - "actualBoundingBoxDescent", - ctx.env.create_double(m.descent as f64)?, - )?; - metrics.set_named_property( - "actualBoundingBoxLeft", - ctx.env.create_double(m.left as f64)?, - )?; - metrics.set_named_property( - "actualBoundingBoxRight", - ctx.env.create_double(m.right as f64)?, - )?; - metrics.set_named_property( - "fontBoundingBoxAscent", - ctx.env.create_double(m.font_ascent as f64)?, - )?; - metrics.set_named_property( - "fontBoundingBoxDescent", - ctx.env.create_double(m.font_descent as f64)?, - )?; - metrics.set_named_property("width", ctx.env.create_double(m.width as f64)?)?; - Ok(metrics) -} + #[napi(setter, return_if_invalid)] + pub fn set_shadow_offset_y(&mut self, offset_y: f64) { + self.context.state.shadow_offset_y = offset_y as f32; + } -#[js_function(2)] -fn move_to(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(getter)] + pub fn get_text_align(&self) -> String { + self.context.state.text_align.as_str().to_owned() + } - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; + #[napi(setter, return_if_invalid)] + pub fn set_text_align(&mut self, align: String) -> Result<()> { + self.context.set_text_align(align)?; + Ok(()) + } - context_2d.path.move_to(x, y); + #[napi(getter)] + pub fn get_text_baseline(&self) -> String { + self.context.state.text_baseline.as_str().to_owned() + } - ctx.env.get_undefined() -} + #[napi(setter, return_if_invalid)] + pub fn set_text_baseline(&mut self, baseline: String) -> Result<()> { + self.context.set_text_baseline(baseline)?; + Ok(()) + } -#[js_function(1)] -fn set_miter_limit(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let miter = ctx.get::(0)?.get_double()? as f32; - context_2d.state.paint.set_stroke_miter(miter); - ctx.env.get_undefined() -} + #[napi] + pub fn arc( + &mut self, + x: f64, + y: f64, + radius: f64, + start_angle: f64, + end_angle: f64, + anticlockwise: Option, + ) { + self.context.arc( + x as f32, + y as f32, + radius as f32, + start_angle as f32, + end_angle as f32, + anticlockwise.unwrap_or(false), + ); + } -#[js_function] -fn get_miter_limit(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi] + pub fn arc_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) { + self + .context + .arc_to(x1 as f32, y1 as f32, x2 as f32, y2 as f32, radius as f32); + } - ctx - .env - .create_double(context_2d.state.paint.get_stroke_miter() as f64) -} + #[napi] + pub fn begin_path(&mut self) { + self.context.begin_path(); + } -#[js_function(4)] -fn stroke_rect(ctx: CallContext) -> Result { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let w = ctx.get::(2)?.get_double()? as f32; - let h = ctx.get::(3)?.get_double()? as f32; + #[napi] + pub fn bezier_curve_to(&mut self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64) { + self.context.bezier_curve_to( + cp1x as f32, + cp1y as f32, + cp2x as f32, + cp2y as f32, + x as f32, + y as f32, + ); + } - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi] + pub fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) { + self + .context + .quadratic_curve_to(cpx as f32, cpy as f32, x as f32, y as f32); + } - context_2d.stroke_rect(x, y, w, h)?; + #[napi] + pub fn clip( + &mut self, + rule_or_path: Option>, + maybe_rule: Option, + ) { + let rule = rule_or_path + .as_ref() + .and_then(|e| match e { + Either::A(s) => FillType::from_str(s).ok(), + Either::B(_) => None, + }) + .or_else(|| maybe_rule.and_then(|s| FillType::from_str(&s).ok())) + .unwrap_or(FillType::Winding); + let path = rule_or_path.and_then(|e| match e { + Either::A(_) => None, + Either::B(p) => Some(p), + }); + self.context.clip(path.map(|p| &mut p.inner), rule); + } - ctx.env.get_undefined() -} + #[napi] + pub fn clear_rect(&mut self, x: f64, y: f64, width: f64, height: f64) { + self + .context + .clear_rect(x as f32, y as f32, width as f32, height as f32); + } -#[js_function(4)] -fn stroke_text(ctx: CallContext) -> Result { - let text = ctx.get::(0)?.into_utf8()?; - let text = text.as_str()?; - if text.is_empty() { - return ctx.env.get_undefined(); - } - let x = ctx.get::(1)?.get_double()? as f32; - let y = ctx.get::(2)?.get_double()? as f32; - let max_width = if ctx.length == 3 { - MAX_TEXT_WIDTH - } else { - ctx.get::(3)?.get_double()? as f32 - }; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.stroke_text(text, x, y, max_width)?; + #[napi] + pub fn close_path(&mut self) { + self.context.close_path(); + } - ctx.env.get_undefined() -} - -#[js_function(4)] -fn fill_rect(ctx: CallContext) -> Result { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - let w = ctx.get::(2)?.get_double()? as f32; - let h = ctx.get::(3)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d.fill_rect(x, y, w, h)?; - - ctx.env.get_undefined() -} - -#[js_function(4)] -fn fill_text(ctx: CallContext) -> Result { - let text = ctx.get::(0)?.into_utf8()?; - let text = text.as_str()?; - if text.is_empty() { - return ctx.env.get_undefined(); - } - let x = ctx.get::(1)?.get_double()? as f32; - let y = ctx.get::(2)?.get_double()? as f32; - let max_width = if ctx.length == 3 { - MAX_TEXT_WIDTH - } else { - ctx.get::(3)?.get_double()? as f32 - }; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.fill_text(text, x, y, max_width)?; - - ctx.env.get_undefined() -} - -#[js_function(5)] -fn get_image_data(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let x = ctx.get::(0)?.get_uint32()?; - let y = ctx.get::(1)?.get_uint32()?; - let width = ctx.get::(2)?.get_uint32()?; - let height = ctx.get::(3)?.get_uint32()?; - let color_space = if ctx.length == 5 { - let image_settings = ctx.get::(4)?; - let cs = image_settings.get_named_property_unchecked::("colorSpace")?; - if cs.get_type()? == ValueType::String { - let color_space_js = unsafe { cs.cast::() }.into_utf8()?; - ColorSpace::from_str(color_space_js.as_str()?)? - } else { - ColorSpace::default() + #[napi] + pub fn create_image_data( + &mut self, + env: Env, + width_or_data: Either, + width_or_height: u32, + height_or_settings: Option>, + maybe_settings: Option, + ) -> Result> { + match width_or_data { + Either::A(width) => { + let height = width_or_height; + let color_space = match height_or_settings { + Some(Either::B(settings)) => { + ColorSpace::from_str(&settings.color_space).unwrap_or_default() + } + _ => ColorSpace::default(), + }; + let arraybuffer_length = (width * height * 4) as usize; + let mut data_buffer = vec![0; arraybuffer_length]; + let data_ptr = data_buffer.as_mut_ptr(); + let data_object = unsafe { + Object::from_raw_unchecked( + env.raw(), + Uint8ClampedArray::to_napi_value(env.raw(), Uint8ClampedArray::new(data_buffer))?, + ) + }; + let instance = ImageData { + width: width as usize, + height: height as usize, + color_space, + data: data_ptr, + } + .into_instance(env)?; + let mut image_instance = unsafe { Object::from_raw_unchecked(env.raw(), instance.raw()) }; + image_instance.set("data", &data_object)?; + Ok(instance) + } + Either::B(mut data_object) => { + let input_data_length = data_object.len(); + let width = width_or_height; + let height = match &height_or_settings { + Some(Either::A(height)) => *height, + _ => (input_data_length as u32) / 4 / width, + }; + let data = data_object.as_mut_ptr(); + let color_space = maybe_settings + .and_then(|settings| ColorSpace::from_str(&settings.color_space).ok()) + .unwrap_or_default(); + let instance = ImageData { + width: width as usize, + height: height as usize, + color_space, + data, + } + .into_instance(env)?; + let mut image_instance = unsafe { Object::from_raw_unchecked(env.raw(), instance.raw()) }; + image_instance.set("data", data_object)?; + Ok(instance) + } } - } else { - ColorSpace::default() - }; - let pixels = context_2d - .surface - .read_pixels(x, y, width, height, color_space) - .ok_or_else(|| { - Error::new( - Status::GenericFailure, - "Read pixels from canvas failed".to_string(), - ) - })?; - let length = pixels.len(); - ctx.env.create_arraybuffer_with_data(pixels).and_then(|ab| { - ab.into_raw() - .into_typedarray(TypedArrayType::Uint8Clamped, length, 0) - }) -} - -#[js_function] -fn get_line_dash(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let line_dash_list = &context_2d.state.line_dash_list; - - let mut arr = ctx.env.create_array_with_length(line_dash_list.len())?; + } - for (index, a) in line_dash_list.iter().enumerate() { - arr.set_element(index as u32, ctx.env.create_double(*a as f64)?)?; + #[napi] + pub fn create_linear_gradient( + &mut self, + env: Env, + x0: f64, + y0: f64, + x1: f64, + y1: f64, + ) -> Result> { + let linear_gradient = + Gradient::create_linear_gradient(x0 as f32, y0 as f32, x1 as f32, y1 as f32); + CanvasGradient(linear_gradient).into_instance(env) } - Ok(arr) -} -#[js_function(7)] -fn put_image_data(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let image_data_js = ctx.get::(0)?; - let image_data = ctx.env.unwrap::(&image_data_js)?; - let dx = ctx.get::(1)?.get_uint32()?; - let dy = ctx.get::(2)?.get_uint32()?; - if ctx.length == 3 { - context_2d.surface.canvas.write_pixels(image_data, dx, dy); - } else { - let mut dirty_x = ctx.get::(3)?.get_double()? as f32; - let mut dirty_y = if ctx.length >= 5 { - ctx.get::(4)?.get_double()? as f32 - } else { - 0.0f32 - }; - let mut dirty_width = if ctx.length >= 6 { - ctx.get::(5)?.get_double()? as f32 - } else { - image_data.width as f32 - }; - let mut dirty_height = if ctx.length == 7 { - ctx.get::(6)?.get_double()? as f32 - } else { - image_data.height as f32 - }; - // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata - if dirty_width < 0f32 { - dirty_x += dirty_width; - dirty_width = dirty_width.abs(); - } - if dirty_height < 0f32 { - dirty_y += dirty_height; - dirty_height = dirty_height.abs(); - } - if dirty_x < 0f32 { - dirty_width += dirty_x; - dirty_x = 0f32; - } - if dirty_y < 0f32 { - dirty_height += dirty_y; - dirty_y = 0f32; - } - if dirty_width <= 0f32 || dirty_height <= 0f32 { - return ctx.env.get_undefined(); - } - let inverted = context_2d.surface.canvas.get_transform_matrix().invert(); - context_2d.surface.canvas.save(); - if let Some(inverted) = inverted { - context_2d.surface.canvas.concat(&inverted); - }; - context_2d.surface.canvas.write_pixels_dirty( - image_data, - dx as f32, - dy as f32, - dirty_x, - dirty_y, - dirty_width, - dirty_height, - image_data.color_space, + #[napi] + pub fn create_radial_gradient( + &mut self, + env: Env, + x0: f64, + y0: f64, + r0: f64, + x1: f64, + y1: f64, + r1: f64, + ) -> Result> { + let radial_gradient = Gradient::create_radial_gradient( + x0 as f32, y0 as f32, r0 as f32, x1 as f32, y1 as f32, r1 as f32, ); - context_2d.surface.canvas.restore(); - }; - - ctx.env.get_undefined() -} - -#[js_function(1)] -fn set_global_alpha(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let alpha = ctx.get::(0)?.get_double()? as f32; - - if !(0.0..=1.0).contains(&alpha) { - return Err(Error::new( - Status::InvalidArg, - format!( - "Alpha value out of range, expected 0.0 - 1.0, but got : {}", - alpha - ), - )); + CanvasGradient(radial_gradient).into_instance(env) } - context_2d.state.paint.set_alpha((alpha * 255.0) as u8); - ctx.env.get_undefined() -} - -#[js_function] -fn get_global_alpha(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_double((context_2d.state.paint.get_alpha() as f64) / 255.0) -} - -#[js_function(1)] -fn set_global_composite_operation(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let blend_string = ctx.get::(0)?.into_utf8()?; - - context_2d - .state - .paint - .set_blend_mode(BlendMode::from_str(blend_string.as_str()?).map_err(Error::from)?); - - ctx.env.get_undefined() -} - -#[js_function] -fn get_global_composite_operation(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_string(context_2d.state.paint.get_blend_mode().as_str()) -} - -#[js_function(1)] -fn set_image_smoothing_enabled(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let enabled = ctx.get::(0)?; - - context_2d.state.image_smoothing_enabled = enabled.get_value()?; - - ctx.env.get_undefined() -} - -#[js_function] -fn get_image_smoothing_enabled(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .get_boolean(context_2d.state.image_smoothing_enabled) -} - -#[js_function(1)] -fn set_image_smoothing_quality(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let quality = ctx.get::(0)?.into_utf8()?; - - context_2d.state.image_smoothing_quality = - FilterQuality::from_str(quality.as_str()?).map_err(Error::from)?; - - ctx.env.get_undefined() -} - -#[js_function] -fn get_image_smoothing_quality(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi] + pub fn create_conic_gradient( + &mut self, + env: Env, + r: f64, + x: f64, + y: f64, + ) -> Result> { + let conic_gradient = Gradient::create_conic_gradient(x as f32, y as f32, r as f32); + CanvasGradient(conic_gradient).into_instance(env) + } - ctx - .env - .create_string(context_2d.state.image_smoothing_quality.as_str()) -} + #[napi] + pub fn create_pattern( + &self, + env: Env, + input: Either4<&mut Image, &mut ImageData, &mut CanvasElement, &mut SVGCanvas>, + repetition: Option, + ) -> Result> { + CanvasPattern::new(input, repetition)?.into_instance(env) + } -#[js_function] -fn get_current_transform(ctx: CallContext) -> Result { - let mut transform_object = ctx.env.create_object()?; - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let current_transform = context_2d.state.transform.get_transform(); - - transform_object.set_named_property("a", ctx.env.create_double(current_transform.a as f64)?)?; - transform_object.set_named_property("b", ctx.env.create_double(current_transform.b as f64)?)?; - transform_object.set_named_property("c", ctx.env.create_double(current_transform.c as f64)?)?; - transform_object.set_named_property("d", ctx.env.create_double(current_transform.d as f64)?)?; - transform_object.set_named_property("e", ctx.env.create_double(current_transform.e as f64)?)?; - transform_object.set_named_property("f", ctx.env.create_double(current_transform.f as f64)?)?; - Ok(transform_object) -} + #[napi] + pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64) { + self + .context + .rect(x as f32, y as f32, width as f32, height as f32); + } -#[js_function(6)] -fn set_current_transform(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let transform = if ctx.length == 1 { - let transform_object = ctx.get::(0)?; - let a = transform_object - .get_named_property::("a")? - .get_double()? as f32; - let b = transform_object - .get_named_property::("b")? - .get_double()? as f32; - let c = transform_object - .get_named_property::("c")? - .get_double()? as f32; - let d = transform_object - .get_named_property::("d")? - .get_double()? as f32; - let e = transform_object - .get_named_property::("e")? - .get_double()? as f32; - let f = transform_object - .get_named_property::("f")? - .get_double()? as f32; - Matrix::new(a, c, e, b, d, f) - } else if ctx.length == 6 { - let a = ctx.get::(0)?.get_double()? as f32; - let b = ctx.get::(1)?.get_double()? as f32; - let c = ctx.get::(2)?.get_double()? as f32; - let d = ctx.get::(3)?.get_double()? as f32; - let e = ctx.get::(4)?.get_double()? as f32; - let f = ctx.get::(5)?.get_double()? as f32; - Matrix::new(a, c, e, b, d, f) - } else { - return Err(Error::new( - Status::InvalidArg, - "Invalid argument length in setTransform".to_string(), - )); - }; - - context_2d.set_transform(transform); + #[napi] + pub fn fill( + &mut self, + rule_or_path: Option>, + maybe_rule: Option, + ) -> Result<()> { + let rule = rule_or_path + .as_ref() + .and_then(|e| match e { + Either::A(s) => FillType::from_str(s).ok(), + Either::B(_) => None, + }) + .or_else(|| maybe_rule.and_then(|s| FillType::from_str(&s).ok())) + .unwrap_or(FillType::Winding); + let path = rule_or_path.and_then(|e| match e { + Either::A(_) => None, + Either::B(p) => Some(p), + }); + self.context.fill(path.map(|p| &mut p.inner), rule)?; + Ok(()) + } - ctx.env.get_undefined() -} + #[napi] + pub fn save(&mut self) { + self.context.save(); + } -#[js_function(2)] -fn scale(ctx: CallContext) -> Result { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; + #[napi(return_if_invalid)] + pub fn restore(&mut self) { + self.context.restore(); + } - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(return_if_invalid)] + pub fn rotate(&mut self, angle: f64) { + self.context.rotate(angle as f32); + } - context_2d.scale(x, y); + #[napi(return_if_invalid)] + pub fn scale(&mut self, x: f64, y: f64) { + self.context.scale(x as f32, y as f32); + } - ctx.env.get_undefined() -} + #[napi] + pub fn draw_image( + &mut self, + image: Either3<&mut CanvasElement, &mut SVGCanvas, &mut Image>, + sx: Option, + sy: Option, + s_width: Option, + s_height: Option, + dx: Option, + dy: Option, + d_width: Option, + d_height: Option, + ) -> Result<()> { + let bitmap = match image { + Either3::A(canvas) => BitmapRef::Owned(canvas.ctx.as_ref().context.surface.get_bitmap()), + Either3::B(svg) => BitmapRef::Owned(svg.ctx.as_ref().context.surface.get_bitmap()), + Either3::C(image) => { + if !image.complete { + return Ok(()); + } + image.regenerate_bitmap_if_need(); + if let Some(bitmap) = &mut image.bitmap { + BitmapRef::Borrowed(bitmap) + } else { + return Ok(()); + } + } + }; + let bitmap_ref = bitmap.as_ref(); + let (sx, sy, s_width, s_height, dx, dy, d_width, d_height) = + match (sx, sy, s_width, s_height, dx, dy, d_width, d_height) { + (Some(dx), Some(dy), None, None, None, None, None, None) => ( + 0.0, + 0.0, + bitmap_ref.0.width as f32, + bitmap_ref.0.height as f32, + dx as f32, + dy as f32, + bitmap_ref.0.width as f32, + bitmap_ref.0.height as f32, + ), + (Some(dx), Some(dy), Some(d_width), Some(d_height), None, None, None, None) => ( + 0.0, + 0.0, + bitmap_ref.0.width as f32, + bitmap_ref.0.height as f32, + dx as f32, + dy as f32, + d_width as f32, + d_height as f32, + ), + ( + Some(sx), + Some(sy), + Some(s_width), + Some(s_height), + Some(dx), + Some(dy), + Some(d_width), + Some(d_height), + ) => ( + sx as f32, + sy as f32, + s_width as f32, + s_height as f32, + dx as f32, + dy as f32, + d_width as f32, + d_height as f32, + ), + _ => return Ok(()), + }; + self.context.draw_image( + bitmap_ref, sx, sy, s_width, s_height, dx, dy, d_width, d_height, + )?; + Ok(()) + } -#[js_function(1)] -fn set_line_dash(ctx: CallContext) -> Result { - let dash = ctx.get::(0)?; - let len = dash.get_array_length()? as usize; - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let is_odd = len & 1 != 0; - let mut dash_list = if is_odd { - vec![0f32; len * 2] - } else { - vec![0f32; len] - }; - for idx in 0..len { - let dash_value: f32 = dash.get_element::(idx as u32)?.get_double()? as f32; - dash_list[idx] = dash_value as f32; - if is_odd { - dash_list[idx + len] = dash_value as f32; + #[napi] + pub fn get_context_attributes(&self) -> ContextAttributes { + ContextAttributes { + alpha: self.context.alpha, + desynchronized: false, } } - context_2d.state.line_dash_list = dash_list; - ctx.env.get_undefined() -} - -#[js_function(1)] -fn stroke(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - if ctx.length == 0 { - context_2d.stroke(None)?; - } else { - let path_js = ctx.get::(0)?; - let path = &mut path_js.unwrap::(ctx.env)?.inner; - context_2d.stroke(Some(path))?; - }; - - ctx.env.get_undefined() -} - -#[js_function(2)] -fn translate(ctx: CallContext) -> Result { - let x = ctx.get::(0)?.get_double()? as f32; - let y = ctx.get::(1)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.translate(x, y); - ctx.env.get_undefined() -} - -#[js_function(6)] -fn transform(ctx: CallContext) -> Result { - let a = ctx.get::(0)?.get_double()? as f32; - let b = ctx.get::(1)?.get_double()? as f32; - let c = ctx.get::(2)?.get_double()? as f32; - let d = ctx.get::(3)?.get_double()? as f32; - let e = ctx.get::(4)?.get_double()? as f32; - let f = ctx.get::(5)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let ts = Matrix::new(a, c, e, b, d, f); - context_2d.transform(ts)?; - ctx.env.get_undefined() -} - -#[js_function] -fn reset_transform(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d.reset_transform(); - ctx.env.get_undefined() -} - -#[js_function] -fn get_line_cap(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_string(context_2d.state.paint.get_stroke_cap().as_str()) -} - -#[js_function(1)] -fn set_line_cap(ctx: CallContext) -> Result { - let line_cap_string = ctx.get::(0)?; - let line_cap = line_cap_string.into_utf8()?; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d - .state - .paint - .set_stroke_cap(StrokeCap::from_str(line_cap.as_str()?)?); - - ctx.env.get_undefined() -} - -#[js_function] -fn get_line_dash_offset(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_double(context_2d.state.line_dash_offset as f64) -} - -#[js_function(1)] -fn set_line_dash_offset(ctx: CallContext) -> Result { - let line_offset = ctx.get::(0)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d.state.line_dash_offset = line_offset as f32; - - ctx.env.get_undefined() -} -#[js_function] -fn get_line_join(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_string(context_2d.state.paint.get_stroke_join().as_str()) -} - -#[js_function(1)] -fn set_line_join(ctx: CallContext) -> Result { - let line_join_string = ctx.get::(0)?; - let line_join = line_join_string.into_utf8()?; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d - .state - .paint - .set_stroke_join(StrokeJoin::from_str(line_join.as_str()?)?); - - ctx.env.get_undefined() -} - -#[js_function] -fn get_line_width(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_double(context_2d.state.paint.get_stroke_width() as f64) -} - -#[js_function(1)] -fn set_line_width(ctx: CallContext) -> Result { - let width = ctx.get::(0)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d.state.paint.set_stroke_width(width as f32); - - ctx.env.get_undefined() -} - -#[js_function(1)] -fn set_fill_style(ctx: CallContext) -> Result { - let mut this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let js_fill_style = ctx.get::(0)?; - - let pattern = match js_fill_style.get_type()? { - ValueType::String => { - let js_color = unsafe { js_fill_style.cast::() }.into_utf8()?; - Pattern::from_color(js_color.as_str()?).ok() - } - ValueType::Object => { - let fill_object = unsafe { js_fill_style.cast::() }; - fill_object - .unwrap::(ctx.env) - .map(|g| Pattern::Gradient(g.0.clone())) - .or_else(|_| ctx.env.unwrap::(&fill_object).map(|p| p.clone())) - .ok() + #[napi] + pub fn is_point_in_path( + &self, + x_or_path: Either, + x_or_y: f64, + y_or_fill_rule: Option>, + maybe_fill_rule: Option, + ) -> Result { + match x_or_path { + Either::A(x) => { + let y = x_or_y; + let fill_rule = y_or_fill_rule + .and_then(|v| match v { + Either::B(rule) => rule.parse().ok(), + _ => None, + }) + .unwrap_or(FillType::Winding); + Ok(self.context.path.hit_test(x as f32, y as f32, fill_rule)) + } + Either::B(path) => { + let x = x_or_y; + let y = match y_or_fill_rule { + Some(Either::A(y)) => y, + _ => { + return Err(Error::new( + Status::InvalidArg, + "The y-axis coordinate of the point to check is missing".to_owned(), + )) + } + }; + let fill_rule = maybe_fill_rule + .and_then(|s| s.parse().ok()) + .unwrap_or(FillType::Winding); + Ok(path.inner.hit_test(x as f32, y as f32, fill_rule)) + } } - _ => None, - }; - - if let Some(p) = pattern { - context_2d.state.fill_style = p; - this.set_named_property("_fillStyle", js_fill_style)?; } - ctx.env.get_undefined() -} - -#[js_function] -fn get_fill_style(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - this.get_named_property("_fillStyle") -} - -#[js_function(1)] -fn set_filter(ctx: CallContext) -> Result { - let filter_str = ctx.get::(0)?.into_utf8()?; - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let filter_str = filter_str.as_str()?; - context_2d.set_filter(filter_str)?; - ctx.env.get_undefined() -} - -#[js_function] -fn get_filter(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - ctx - .env - .create_string(context_2d.state.filters_string.as_str()) -} - -#[js_function] -fn get_font(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx.env.create_string(context_2d.state.font.as_str()) -} - -#[js_function(1)] -fn set_font(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - let last_state = &mut context_2d.state; - let font_style = ctx.get::(0)?.into_utf8()?.into_owned()?; - last_state.font_style = - Font::new(font_style.as_str()).map_err(|e| Error::new(Status::InvalidArg, format!("{}", e)))?; - - last_state.font = font_style; - ctx.env.get_undefined() -} + #[napi] + pub fn is_point_in_stroke( + &self, + x_or_path: Either, + x_or_y: f64, + maybe_y: Option, + ) -> Result { + let stroke_w = self.context.get_stroke_width(); + match x_or_path { + Either::A(x) => { + let y = x_or_y; + Ok( + self + .context + .path + .stroke_hit_test(x as f32, y as f32, stroke_w), + ) + } + Either::B(path) => { + let x = x_or_y; + if let Some(y) = maybe_y { + Ok(path.inner.stroke_hit_test(x as f32, y as f32, stroke_w)) + } else { + Err(Error::new( + Status::InvalidArg, + "The y-axis coordinate of the point to check is missing".to_owned(), + )) + } + } + } + } -#[js_function] -fn get_text_direction(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(return_if_invalid)] + pub fn ellipse( + &mut self, + x: f64, + y: f64, + radius_x: f64, + radius_y: f64, + rotation: f64, + start_angle: f64, + end_angle: f64, + anticlockwise: Option, + ) { + self.context.ellipse( + x as f32, + y as f32, + radius_x as f32, + radius_y as f32, + rotation as f32, + start_angle as f32, + end_angle as f32, + anticlockwise.unwrap_or(false), + ); + } - let last_state = &context_2d.state; - ctx.env.create_string(last_state.text_direction.as_str()) -} + #[napi(return_if_invalid)] + pub fn line_to(&mut self, x: f64, y: f64) { + if !x.is_nan() && !x.is_infinite() && !y.is_nan() && !y.is_infinite() { + self.context.path.line_to(x as f32, y as f32); + } + } -#[js_function(1)] -fn set_text_direction(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let direction = ctx.get::(0)?.into_utf8()?; - let text_direction = TextDirection::from_str(direction.as_str()?)?; - let last_state = &mut context_2d.state; - last_state.text_direction = text_direction; - ctx.env.get_undefined() -} + #[napi] + pub fn measure_text(&mut self, text: String) -> Result { + if text.is_empty() { + return Ok(TextMetrics { + actual_bounding_box_ascent: 0.0, + actual_bounding_box_descent: 0.0, + actual_bounding_box_left: 0.0, + actual_bounding_box_right: 0.0, + font_bounding_box_ascent: 0.0, + font_bounding_box_descent: 0.0, + width: 0.0, + }); + } + let metrics = self.context.get_line_metrics(&text)?; + Ok(TextMetrics { + actual_bounding_box_ascent: metrics.0.ascent as f64, + actual_bounding_box_descent: metrics.0.descent as f64, + actual_bounding_box_left: metrics.0.left as f64, + actual_bounding_box_right: metrics.0.right as f64, + font_bounding_box_ascent: metrics.0.font_ascent as f64, + font_bounding_box_descent: metrics.0.font_descent as f64, + width: metrics.0.width as f64, + }) + } -#[js_function(1)] -fn set_stroke_style(ctx: CallContext) -> Result { - let mut this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi(return_if_invalid)] + pub fn move_to(&mut self, x: f64, y: f64) { + if !x.is_nan() && !x.is_infinite() && !y.is_nan() && !y.is_infinite() { + self.context.path.move_to(x as f32, y as f32); + } + } - let js_stroke_style = ctx.get::(0)?; - let last_state = &mut context_2d.state; + #[napi(return_if_invalid)] + pub fn fill_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<()> { + if !x.is_nan() + && !x.is_infinite() + && !y.is_nan() + && !y.is_infinite() + && !width.is_nan() + && !width.is_infinite() + && !height.is_nan() + && !height.is_infinite() + { + self + .context + .fill_rect(x as f32, y as f32, width as f32, height as f32)?; + } + Ok(()) + } - let pattern = match js_stroke_style.get_type()? { - ValueType::String => { - let js_color = unsafe { JsString::from_raw_unchecked(ctx.env.raw(), js_stroke_style.raw()) } - .into_utf8()?; - Pattern::from_color(js_color.as_str()?).ok() + #[napi(return_if_invalid)] + pub fn fill_text(&mut self, text: String, x: f64, y: f64, max_width: Option) -> Result<()> { + if text.is_empty() { + return Ok(()); } - ValueType::Object => { - let stroke_object = unsafe { js_stroke_style.cast::() }; - stroke_object - .unwrap::(ctx.env) - .map(|g| Pattern::Gradient(g.0.clone())) - .or_else(|_| ctx.env.unwrap::(&stroke_object).map(|p| p.clone())) - .ok() + if !x.is_nan() && !x.is_infinite() && !y.is_nan() && !y.is_infinite() { + self.context.fill_text( + &text, + x as f32, + y as f32, + max_width.map(|f| f as f32).unwrap_or(MAX_TEXT_WIDTH), + )?; } - _ => None, - }; - - if let Some(p) = pattern { - last_state.stroke_style = p; - this.set_named_property("_strokeStyle", js_stroke_style)?; + Ok(()) } - ctx.env.get_undefined() -} - -#[js_function] -fn get_stroke_style(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - this.get_named_property("_strokeStyle") -} - -#[js_function] -fn get_shadow_blur(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let last_state = &mut context_2d.state; - - ctx.env.create_double(last_state.shadow_blur as f64) -} - -#[js_function(1)] -fn set_shadow_blur(ctx: CallContext) -> Result { - let blur = ctx.get::(0)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d.state.shadow_blur = blur; - - ctx.env.get_undefined() -} - -#[js_function] -fn get_shadow_color(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_string(context_2d.state.shadow_color_string.as_str()) -} - -#[js_function(1)] -fn set_shadow_color(ctx: CallContext) -> Result { - let shadow_color_string = ctx.get::(0)?; - let shadow_color = shadow_color_string.into_utf8()?; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; + #[napi] + pub fn stroke(&mut self, path: Option<&mut Path>) -> Result<()> { + self.context.stroke(path.map(|p| &mut p.inner))?; + Ok(()) + } - let last_state = &mut context_2d.state; - let shadow_color_str = shadow_color.as_str()?; - last_state.shadow_color_string = shadow_color_str.to_owned(); + #[napi(return_if_invalid)] + pub fn stroke_rect(&mut self, x: f64, y: f64, width: f64, height: f64) -> Result<()> { + if !x.is_nan() + && !x.is_infinite() + && !y.is_nan() + && !y.is_infinite() + && !width.is_nan() + && !width.is_infinite() + && !height.is_nan() + && !height.is_infinite() + { + self + .context + .stroke_rect(x as f32, y as f32, width as f32, height as f32)?; + } + Ok(()) + } - if shadow_color_str.is_empty() { - return ctx.env.get_undefined(); + #[napi(return_if_invalid)] + pub fn stroke_text( + &mut self, + text: String, + x: f64, + y: f64, + max_width: Option, + ) -> Result<()> { + if text.is_empty() { + return Ok(()); + } + if !x.is_nan() && !x.is_infinite() && !y.is_nan() && !y.is_infinite() { + self.context.stroke_text( + &text, + x as f32, + y as f32, + max_width.map(|v| v as f32).unwrap_or(MAX_TEXT_WIDTH), + )?; + } + Ok(()) } - let mut parser_input = ParserInput::new(shadow_color_str); - let mut parser = Parser::new(&mut parser_input); - let color = CSSColor::parse(&mut parser) - .map_err(|e| SkError::Generic(format!("Parse color [{}] error: {:?}", shadow_color_str, e)))?; - match color { - CSSColor::CurrentColor => { - return Err(Error::new( + #[napi] + pub fn get_image_data( + &mut self, + env: Env, + x: f64, + y: f64, + width: f64, + height: f64, + color_space: Option, + ) -> Result> { + if !x.is_nan() + && !x.is_infinite() + && !y.is_nan() + && !y.is_infinite() + && !width.is_nan() + && !width.is_infinite() + && !height.is_nan() + && !height.is_infinite() + { + let color_space = color_space + .and_then(|cs| cs.parse().ok()) + .unwrap_or(ColorSpace::Srgb); + let mut image_data = self + .context + .get_image_data(x as f32, y as f32, width as f32, height as f32, color_space) + .ok_or_else(|| { + Error::new( + Status::GenericFailure, + "Read pixels from canvas failed".to_string(), + ) + })?; + let data = image_data.as_mut_ptr(); + let data_object = unsafe { + Object::from_raw_unchecked( + env.raw(), + Uint8ClampedArray::to_napi_value(env.raw(), Uint8ClampedArray::new(image_data))?, + ) + }; + let instance = ImageData { + width: width as usize, + height: height as usize, + color_space, + data, + } + .into_instance(env)?; + let mut image_instance = unsafe { Object::from_raw_unchecked(env.raw(), instance.raw()) }; + image_instance.set("data", data_object)?; + Ok(instance) + } else { + Err(Error::new( Status::InvalidArg, - "Color should not be `currentcolor` keyword".to_owned(), + "The x, y, width, and height arguments must be finite numbers".to_owned(), )) } - CSSColor::RGBA(rgba) => { - last_state.shadow_color = rgba; - } } - ctx.env.get_undefined() -} - -#[js_function] -fn get_shadow_offset_x(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let last_state = &mut context_2d.state; + #[napi] + pub fn get_line_dash(&self) -> Vec { + self + .context + .state + .line_dash_list + .iter() + .map(|l| *l as f64) + .collect() + } - ctx.env.create_double(last_state.shadow_offset_x as f64) -} + #[napi] + pub fn put_image_data( + &mut self, + image_data: &mut ImageData, + dx: u32, + dy: u32, + dirty_x: Option, + dirty_y: Option, + dirty_width: Option, + dirty_height: Option, + ) { + if let Some(dirty_x) = dirty_x { + let mut dirty_x = dirty_x as f32; + let mut dirty_y = dirty_y.map(|d| d as f32).unwrap_or(0.0); + let mut dirty_width = dirty_width + .map(|d| d as f32) + .unwrap_or(image_data.width as f32); + let mut dirty_height = dirty_height + .map(|d| d as f32) + .unwrap_or(image_data.height as f32); + // as per https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-putimagedata + if dirty_width < 0f32 { + dirty_x += dirty_width; + dirty_width = dirty_width.abs(); + } + if dirty_height < 0f32 { + dirty_y += dirty_height; + dirty_height = dirty_height.abs(); + } + if dirty_x < 0f32 { + dirty_width += dirty_x; + dirty_x = 0f32; + } + if dirty_y < 0f32 { + dirty_height += dirty_y; + dirty_y = 0f32; + } + if dirty_width <= 0f32 || dirty_height <= 0f32 { + return; + } + let inverted = self.context.surface.canvas.get_transform_matrix().invert(); + self.context.surface.canvas.save(); + if let Some(inverted) = inverted { + self.context.surface.canvas.concat(&inverted); + }; + self.context.surface.canvas.write_pixels_dirty( + image_data, + dx as f32, + dy as f32, + dirty_x, + dirty_y, + dirty_width, + dirty_height, + image_data.color_space, + ); + self.context.surface.canvas.restore(); + } else { + self.context.surface.canvas.write_pixels(image_data, dx, dy); + } + } -#[js_function(1)] -fn set_shadow_offset_x(ctx: CallContext) -> Result { - let offset: f32 = ctx.get::(0)?.get_double()? as f32; + #[napi] + pub fn set_line_dash(&mut self, dash_list: Vec) { + let len = dash_list.len(); + let is_odd = len & 1 != 0; + let mut line_dash_list = if is_odd { + vec![0f32; len * 2] + } else { + vec![0f32; len] + }; + for (idx, dash) in dash_list.iter().enumerate() { + line_dash_list[idx] = *dash as f32; + if is_odd { + line_dash_list[idx + len] = *dash as f32; + } + } + self.context.set_line_dash(line_dash_list); + } - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let last_state = &mut context_2d.state; + #[napi] + pub fn reset_transform(&mut self) { + self.context.reset_transform(); + } - last_state.shadow_offset_x = offset; + #[napi(return_if_invalid)] + pub fn translate(&mut self, x: f64, y: f64) { + self.context.translate(x as f32, y as f32); + } - ctx.env.get_undefined() -} + #[napi(return_if_invalid)] + pub fn transform(&mut self, a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> Result<()> { + let ts = Matrix::new(a as f32, c as f32, e as f32, b as f32, d as f32, f as f32); + self.context.transform(ts)?; + Ok(()) + } -#[js_function] -fn get_shadow_offset_y(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let last_state = &mut context_2d.state; + #[napi] + pub fn get_transform(&self) -> TransformObject { + self.context.state.transform.get_transform().into() + } - ctx.env.create_double(last_state.shadow_offset_y as f64) + #[napi] + pub fn set_transform( + &mut self, + a_or_transform: Either, + b: Option, + c: Option, + d: Option, + e: Option, + f: Option, + ) -> Option<()> { + let ts = match a_or_transform { + Either::A(a) => Transform::new( + a as f32, c? as f32, e? as f32, b? as f32, d? as f32, f? as f32, + ), + Either::B(transform) => transform.into(), + }; + self + .context + .set_transform(Matrix::new(ts.a, ts.b, ts.c, ts.d, ts.e, ts.f)); + None + } } -#[js_function(1)] -fn set_shadow_offset_y(ctx: CallContext) -> Result { - let offset = ctx.get::(0)?.get_double()? as f32; - - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - let last_state = &mut context_2d.state; - - last_state.shadow_offset_y = offset; - +#[js_function(4)] +fn context_2d_constructor(ctx: CallContext) -> Result { ctx.env.get_undefined() } -#[js_function(1)] -fn set_text_align(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - context_2d.state.text_align = - TextAlign::from_str(ctx.get::(0)?.into_utf8()?.as_str()?)?; - ctx.env.get_undefined() +enum BitmapRef<'a> { + Borrowed(&'a mut Bitmap), + Owned(Bitmap), } -#[js_function] -fn get_text_align(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx.env.create_string(context_2d.state.text_align.as_str()) +impl AsRef for BitmapRef<'_> { + fn as_ref(&self) -> &Bitmap { + match self { + BitmapRef::Borrowed(bitmap) => bitmap, + BitmapRef::Owned(bitmap) => bitmap, + } + } } -#[js_function(1)] -fn set_text_baseline(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - context_2d.state.text_baseline = - TextBaseline::from_str(ctx.get::(0)?.into_utf8()?.as_str()?)?; - ctx.env.get_undefined() +#[napi(object)] +pub struct TextMetrics { + pub actual_bounding_box_ascent: f64, + pub actual_bounding_box_descent: f64, + pub actual_bounding_box_left: f64, + pub actual_bounding_box_right: f64, + pub font_bounding_box_ascent: f64, + pub font_bounding_box_descent: f64, + pub width: f64, +} + +#[napi(object)] +pub struct TransformObject { + pub a: f64, + pub b: f64, + pub c: f64, + pub d: f64, + pub e: f64, + pub f: f64, +} + +impl From for Transform { + fn from(value: TransformObject) -> Self { + Self::new( + value.a as f32, + value.b as f32, + value.c as f32, + value.d as f32, + value.e as f32, + value.f as f32, + ) + } } -#[js_function] -fn get_text_baseline(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let context_2d = ctx.env.unwrap::(&this)?; - - ctx - .env - .create_string(context_2d.state.text_baseline.as_str()) +impl From for TransformObject { + fn from(value: Transform) -> Self { + Self { + a: value.a as f64, + b: value.b as f64, + c: value.c as f64, + d: value.d as f64, + e: value.e as f64, + f: value.f as f64, + } + } } const AVIF_DEFAULT_QUALITY: f32 = 80.0; diff --git a/src/error.rs b/src/error.rs index 1748599c..39a17703 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,6 +30,10 @@ pub enum SkError { InvalidTransform(Matrix), #[error("Convert String to CString failed")] NulError, + #[error("[`{0}`] is not valid font style")] + InvalidFontStyle(String), + #[error("[`{0}`] is not valid font variant")] + InvalidFontVariant(String), #[error("[`{0}`]")] Generic(String), } diff --git a/src/font.rs b/src/font.rs index a25e838d..bdfe4e84 100644 --- a/src/font.rs +++ b/src/font.rs @@ -2,20 +2,13 @@ use std::str::FromStr; use once_cell::sync::OnceCell; use regex::Regex; -use thiserror::Error; + +use crate::error::SkError; pub(crate) static FONT_REGEXP: OnceCell = OnceCell::new(); const DEFAULT_FONT: &str = "sans-serif"; -#[derive(Error, Clone, Debug)] -pub enum ParseError { - #[error("[`{0}`] is not valid font style")] - InvalidFontStyle(String), - #[error("[`{0}`] is not valid font variant")] - InvalidFontVariant(String), -} - /// The minimum font-weight value per: /// /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values @@ -53,7 +46,7 @@ impl Default for Font { } impl Font { - pub fn new(font_rules: &str) -> Result { + pub fn new(font_rules: &str) -> Result { let font_regexp = FONT_REGEXP.get_or_init(init_font_regexp); let default_font = Font::default(); if let Some(cap) = font_regexp.captures(font_rules) { @@ -112,10 +105,10 @@ impl Font { .join(","), }) } else { - Err(ParseError::InvalidFontStyle(font_rules.to_owned())) + Err(SkError::InvalidFontStyle(font_rules.to_owned())) } } else { - Err(ParseError::InvalidFontStyle(font_rules.to_owned())) + Err(SkError::InvalidFontStyle(font_rules.to_owned())) } } } @@ -160,14 +153,14 @@ impl FontStyle { } impl FromStr for FontStyle { - type Err = ParseError; + type Err = SkError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { "normal" => Ok(Self::Normal), "italic" => Ok(Self::Italic), "oblique" => Ok(Self::Oblique), - _ => Err(ParseError::InvalidFontStyle(s.to_owned())), + _ => Err(SkError::InvalidFontStyle(s.to_owned())), } } } @@ -179,13 +172,13 @@ pub enum FontVariant { } impl FromStr for FontVariant { - type Err = ParseError; + type Err = SkError; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { match s { "normal" => Ok(Self::Normal), "small-caps" => Ok(Self::SmallCaps), - _ => Err(ParseError::InvalidFontVariant(s.to_owned())), + _ => Err(SkError::InvalidFontVariant(s.to_owned())), } } } diff --git a/src/gradient.rs b/src/gradient.rs index be07baa4..342300ef 100644 --- a/src/gradient.rs +++ b/src/gradient.rs @@ -12,13 +12,13 @@ use crate::{ }; #[derive(Debug, Clone)] -pub enum CanvasGradient { +pub enum Gradient { Linear(LinearGradient), Radial(RadialGradient), Conic(ConicGradient), } -impl CanvasGradient { +impl Gradient { pub fn create_linear_gradient(x0: f32, y0: f32, x1: f32, y1: f32) -> Self { let linear_gradient = LinearGradient { start_point: (x0, y0), @@ -155,10 +155,10 @@ impl CanvasGradient { } #[napi] -pub struct Gradient(pub(crate) CanvasGradient); +pub struct CanvasGradient(pub(crate) Gradient); #[napi] -impl Gradient { +impl CanvasGradient { #[napi] pub fn add_color_stop(&mut self, index: f64, color: String) -> Result<()> { if color.is_empty() { @@ -189,12 +189,12 @@ impl Gradient { #[test] fn test_add_color_stop() { - let mut linear_gradient = CanvasGradient::create_linear_gradient(0.0, 0.0, 0.0, 77.0); + let mut linear_gradient = Gradient::create_linear_gradient(0.0, 0.0, 0.0, 77.0); linear_gradient.add_color_stop(1.0, Color::from_rgba(0, 128, 128, 255)); linear_gradient.add_color_stop(0.6, Color::from_rgba(0, 255, 255, 255)); linear_gradient.add_color_stop(0.3, Color::from_rgba(176, 199, 45, 255)); linear_gradient.add_color_stop(0.0, Color::from_rgba(204, 82, 50, 255)); - if let CanvasGradient::Linear(linear_gradient) = linear_gradient { + if let Gradient::Linear(linear_gradient) = linear_gradient { assert_eq!(linear_gradient.base.positions, vec![0.0, 0.3, 0.6, 1.0]); assert_eq!( linear_gradient.base.colors, diff --git a/src/image.rs b/src/image.rs index d1d13674..3eae7dae 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,137 +1,119 @@ -use std::mem::ManuallyDrop; -use std::slice; use std::str; use std::str::FromStr; use base64::decode; -use napi::*; +use napi::{bindgen_prelude::*, NapiValue}; -use crate::ctx::ImageOrCanvas; use crate::sk::Bitmap; use crate::sk::ColorSpace; -#[derive(Debug, Clone)] +#[napi] pub struct ImageData { pub(crate) width: usize, pub(crate) height: usize, - pub(crate) data: *const u8, pub(crate) color_space: ColorSpace, + pub(crate) data: *mut u8, } -impl Drop for ImageData { - fn drop(&mut self) { - let len = (self.width * self.height * 4) as usize; - unsafe { Vec::from_raw_parts(self.data as *mut u8, len, len) }; - } +#[napi(object)] +pub struct Settings { + pub color_space: String, } +#[napi] impl ImageData { - pub fn create_js_class(env: &Env) -> Result { - env.define_class("ImageData", image_data_constructor, &[]) - } -} - -#[js_function(3)] -fn image_data_constructor(ctx: CallContext) -> Result { - let first_arg = ctx.get::(0)?; - let first_arg_type = first_arg.get_type()?; - let ((js_width, width), (js_height, height), arraybuffer_length, mut initial_data, color_space) = - match first_arg_type { - ValueType::Number => { - let js_width = unsafe { first_arg.cast::() }; - let js_height = ctx.get::(1)?; - let color_space = if ctx.length == 3 { - let image_settings = ctx.get::(2)?; - let js_color_space = image_settings - .get_named_property::("colorSpace")? - .into_utf8()?; - ColorSpace::from_str(js_color_space.as_str()?)? - } else { - ColorSpace::default() + #[napi(constructor)] + pub fn new( + env: Env, + mut this: This, + width_or_data: Either, + width_or_height: u32, + height_or_settings: Option>, + maybe_settings: Option, + ) -> Result { + match width_or_data { + Either::A(width) => { + let height = width_or_height; + let color_space = match height_or_settings { + Some(Either::B(settings)) => { + ColorSpace::from_str(&settings.color_space).unwrap_or_default() + } + _ => ColorSpace::default(), }; - let width = js_width.get_uint32()?; - let height = js_height.get_uint32()?; let arraybuffer_length = (width * height * 4) as usize; - Ok(( - (js_width, width), - (js_height, height), - arraybuffer_length, - ManuallyDrop::new(vec![0u8; arraybuffer_length]), + let mut data_buffer = vec![0; arraybuffer_length]; + let data_ptr = data_buffer.as_mut_ptr(); + let data_object = unsafe { + Object::from_raw_unchecked( + env.raw(), + Uint8ClampedArray::to_napi_value(env.raw(), Uint8ClampedArray::new(data_buffer))?, + ) + }; + this.define_properties(&[Property::new("data")? + .with_value(&data_object) + .with_property_attributes( + PropertyAttributes::Enumerable | PropertyAttributes::Configurable, + )])?; + Ok(ImageData { + width: width as usize, + height: height as usize, color_space, - )) + data: data_ptr, + }) } - ValueType::Object => { - let image_data_ab = unsafe { first_arg.cast::() }.into_value()?; - let arraybuffer: &[u8] = image_data_ab.as_ref(); - let arraybuffer_length = arraybuffer.len(); - let js_width = ctx.get::(1)?; - let width = js_width.get_uint32()?; - let (js_height, height) = if ctx.length >= 3 { - let js_height = ctx.get::(2)?; - let height = js_height.get_uint32()?; - if height * width * 4 != arraybuffer_length as u32 { - return Err(Error::new( - Status::InvalidArg, - "Index or size is negative or greater than the allowed amount".to_owned(), - )); - } - (js_height, height) - } else { - let height = arraybuffer_length as u32 / width / 4u32; - (ctx.env.create_uint32(height)?, height) + Either::B(data_object) => { + let input_data_length = data_object.len(); + let width = width_or_height; + let height = match &height_or_settings { + Some(Either::A(height)) => *height, + _ => (input_data_length as u32) / 4 / width, }; - Ok(( - (js_width, width), - (js_height, height), - arraybuffer_length, - ManuallyDrop::new(unsafe { - slice::from_raw_parts(arraybuffer.as_ptr() as *const u8, arraybuffer_length).to_owned() - }), - ColorSpace::default(), - )) + if height * width * 4 != data_object.len() as u32 { + return Err(Error::new( + Status::InvalidArg, + "Index or size is negative or greater than the allowed amount".to_owned(), + )); + } + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createImageData + // An existing ImageData object from which to copy the width and height. + let mut cloned_data = Uint8ClampedArray::new(data_object.to_vec()); + let data = cloned_data.as_mut_ptr(); + this.define_properties(&[Property::new("data")? + .with_value(&unsafe { + Object::from_raw_unchecked( + env.raw(), + Uint8ClampedArray::to_napi_value(env.raw(), cloned_data)?, + ) + }) + .with_property_attributes( + PropertyAttributes::Enumerable | PropertyAttributes::Configurable, + )])?; + let color_space = maybe_settings + .and_then(|settings| ColorSpace::from_str(&settings.color_space).ok()) + .unwrap_or_default(); + Ok(ImageData { + width: width as usize, + height: height as usize, + color_space, + data, + }) } - _ => Err(Error::new( - Status::InvalidArg, - format!( - "Invalid type of first argument of ImageData constructor [{:?}]", - first_arg_type - ), - )), - }?; - let data_ptr = initial_data.as_mut_ptr(); - let image_data = ImageData { - width: width as usize, - height: height as usize, - data: data_ptr, - color_space, - }; - let arraybuffer = unsafe { - ctx - .env - .create_arraybuffer_with_borrowed_data(data_ptr, arraybuffer_length, 0, noop_finalize) - }?; - let typed_array = - arraybuffer - .into_raw() - .into_typedarray(TypedArrayType::Uint8Clamped, arraybuffer_length, 0)?; + } + } + + #[napi(getter)] + pub fn get_width(&self) -> u32 { + self.width as u32 + } - let mut this = ctx.this_unchecked::(); - ctx.env.wrap(&mut this, image_data)?; - this.define_properties(&[ - Property::new("data")? - .with_value(&typed_array) - .with_property_attributes(PropertyAttributes::Enumerable), - Property::new("width")? - .with_value(&js_width) - .with_property_attributes(PropertyAttributes::Enumerable), - Property::new("height")? - .with_value(&js_height) - .with_property_attributes(PropertyAttributes::Enumerable), - ])?; - ctx.env.get_undefined() + #[napi(getter)] + pub fn get_height(&self) -> u32 { + self.height as u32 + } } -pub(crate) struct Image { +#[napi] +pub struct Image { pub(crate) bitmap: Option, pub(crate) complete: bool, pub(crate) alt: String, @@ -140,271 +122,186 @@ pub(crate) struct Image { pub(crate) need_regenerate_bitmap: bool, pub(crate) is_svg: bool, pub(crate) color_space: ColorSpace, + pub(crate) src: Option, } +#[napi] impl Image { - pub(crate) fn regenerate_bitmap_if_need(&mut self, data: D) - where - D: AsRef<[u8]>, - { - if !self.need_regenerate_bitmap || !self.is_svg { - return; - } - 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, - ); + #[napi(constructor)] + pub fn new(width: Option, height: Option, color_space: Option) -> Result { + let width = width.unwrap_or(-1.0); + let height = height.unwrap_or(-1.0); + let color_space = color_space + .and_then(|c| ColorSpace::from_str(&c).ok()) + .unwrap_or_default(); + Ok(Image { + complete: false, + bitmap: None, + alt: "".to_string(), + width, + height, + need_regenerate_bitmap: false, + is_svg: false, + color_space, + src: None, + }) } -} -impl Image { - pub fn create_js_class(env: &Env) -> Result { - env.define_class( - "Image", - image_constructor, - &vec![ - Property::new("width")? - .with_getter(get_width) - .with_setter(set_width), - Property::new("height")? - .with_getter(get_height) - .with_setter(set_height), - Property::new("naturalWidth")? - .with_getter(get_natural_width) - .with_property_attributes(PropertyAttributes::Enumerable), - Property::new("naturalHeight")? - .with_getter(get_natural_height) - .with_property_attributes(PropertyAttributes::Enumerable), - Property::new("complete")? - .with_getter(get_complete) - .with_property_attributes(PropertyAttributes::Enumerable), - Property::new("alt")? - .with_setter(set_alt) - .with_getter(get_alt), - Property::new("src")? - .with_setter(set_src) - .with_getter(get_src), - ], - ) + #[napi(getter)] + pub fn get_width(&self) -> f64 { + if self.width >= 0.0 { + self.width + } else { + 0.0 + } } -} - -#[js_function(3)] -fn image_constructor(ctx: CallContext) -> Result { - let width = if ctx.length > 0 { - ctx.get::(0)?.get_double()? - } else { - -1.0 - }; - let height = if ctx.length > 1 { - ctx.get::(1)?.get_double()? - } else { - -1.0 - }; - let color_space = if ctx.length == 3 { - let color_space = ctx.get::(2)?.into_utf8()?; - ColorSpace::from_str(color_space.as_str()?)? - } else { - ColorSpace::default() - }; - let js_image = Image { - complete: false, - bitmap: None, - alt: "".to_string(), - width, - height, - need_regenerate_bitmap: false, - is_svg: false, - color_space, - }; - let mut this = ctx.this_unchecked::(); - this.set_named_property("_src", ctx.env.get_undefined()?)?; - ctx.env.wrap(&mut this, ImageOrCanvas::Image(js_image))?; - ctx.env.get_undefined() -} - -#[js_function] -fn get_width(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - - ctx - .env - .create_double(if image.width <= 0.0 { 0.0 } else { image.width }) -} - -#[js_function] -fn get_natural_width(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - - ctx - .env - .create_double(image.bitmap.as_ref().map(|b| b.0.width).unwrap_or(0) as f64) -} -#[js_function(1)] -fn set_width(ctx: CallContext) -> Result { - let width = ctx.get::(0)?.get_double()?; - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - if (width - image.width).abs() > f64::EPSILON { - image.width = width; - image.need_regenerate_bitmap = true; + #[napi(setter)] + pub fn set_width(&mut self, width: f64) { + if (width - self.width).abs() > f64::EPSILON { + self.width = width; + self.need_regenerate_bitmap = true; + } } - ctx.env.get_undefined() -} - -#[js_function] -fn get_height(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - - ctx.env.create_double(if image.height <= 0.0 { - 0.0 - } else { - image.height - }) -} - -#[js_function] -fn get_natural_height(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - ctx - .env - .create_double(image.bitmap.as_ref().map(|b| b.0.height).unwrap_or(0) as f64) -} - -#[js_function(1)] -fn set_height(ctx: CallContext) -> Result { - let height = ctx.get::(0)?.get_double()?; - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - if (image.height - height).abs() > f64::EPSILON { - image.height = height; - image.need_regenerate_bitmap = true; + #[napi(getter)] + pub fn get_natural_width(&self) -> f64 { + self.bitmap.as_ref().map(|b| b.0.width).unwrap_or(0) as f64 } - ctx.env.get_undefined() -} -#[js_function] -fn get_complete(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - - ctx.env.get_boolean(image.complete) -} + #[napi(getter)] + pub fn get_height(&self) -> f64 { + if self.height >= 0.0 { + self.height + } else { + 0.0 + } + } -#[js_function] -fn get_alt(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); + #[napi(setter)] + pub fn set_height(&mut self, height: f64) { + if (height - self.height).abs() > f64::EPSILON { + self.height = height; + self.need_regenerate_bitmap = true; + } + } - ctx.env.create_string(image.alt.as_str()) -} + #[napi(getter)] + pub fn get_natural_height(&self) -> f64 { + self.bitmap.as_ref().map(|b| b.0.height).unwrap_or(0) as f64 + } -#[js_function(1)] -fn set_alt(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); - let arg = ctx.get::(0)?.into_utf8()?; - image.alt = arg.as_str()?.to_string(); + #[napi(getter)] + pub fn get_complete(&self) -> bool { + self.complete + } - ctx.env.get_undefined() -} + #[napi(getter)] + pub fn get_alt(&self) -> String { + self.alt.clone() + } -#[js_function] -fn get_src(ctx: CallContext) -> Result { - let this = ctx.this_unchecked::(); - this.get_named_property("_src") -} + #[napi(setter)] + pub fn set_alt(&mut self, alt: String) { + self.alt = alt; + } -#[js_function(1)] -fn set_src(ctx: CallContext) -> Result { - let mut this = ctx.this_unchecked::(); - let src_arg = ctx.get::(0)?; - let src_data = src_arg.into_value()?; - let image = ctx.env.unwrap::(&this)?.get_image().unwrap(); + #[napi(getter)] + pub fn get_src(&mut self) -> Option<&mut Buffer> { + self.src.as_mut() + } - let length = src_data.len(); - let data_ref: &[u8] = &src_data; - let mut is_svg = false; - for i in 3..length { - if '<' == data_ref[i - 3] as char { - match data_ref[i - 2] as char { - '?' | '!' => continue, - 's' => { - is_svg = 'v' == data_ref[i - 1] as char && 'g' == data_ref[i] as char; - break; - } - _ => { - is_svg = false; + #[napi(setter)] + pub fn set_src(&mut self, this: This, data: Buffer) -> Result<()> { + let length = data.len(); + let data_ref: &[u8] = &data; + let mut is_svg = false; + for i in 3..length { + if '<' == data_ref[i - 3] as char { + match data_ref[i - 2] as char { + '?' | '!' => continue, + 's' => { + is_svg = 'v' == data_ref[i - 1] as char && 'g' == data_ref[i] as char; + break; + } + _ => { + is_svg = false; + } } } } - } - image.complete = true; - image.is_svg = is_svg; - if is_svg { - let bitmap = - if (image.width - -1.0).abs() > f64::EPSILON && (image.height - -1.0).abs() > f64::EPSILON { - Bitmap::from_svg_data_with_custom_size( - src_data.as_ptr(), - length, - image.width as f32, - image.height as f32, - image.color_space, - ) - } else { - Bitmap::from_svg_data(src_data.as_ptr(), length, image.color_space) - }; - if let Some(b) = bitmap.as_ref() { - if (image.width - -1.0).abs() < f64::EPSILON { - image.width = b.0.width as f64; - } - if (image.height - -1.0).abs() < f64::EPSILON { - image.height = b.0.height as f64; + self.complete = true; + self.is_svg = is_svg; + if is_svg { + let bitmap = + if (self.width - -1.0).abs() > f64::EPSILON && (self.height - -1.0).abs() > f64::EPSILON { + Bitmap::from_svg_data_with_custom_size( + data.as_ptr(), + length, + self.width as f32, + self.height as f32, + self.color_space, + ) + } else { + Bitmap::from_svg_data(data.as_ptr(), length, self.color_space) + }; + if let Some(b) = bitmap.as_ref() { + if (self.width - -1.0).abs() < f64::EPSILON { + self.width = b.0.width as f64; + } + if (self.height - -1.0).abs() < f64::EPSILON { + self.height = b.0.height as f64; + } } - } - image.bitmap = bitmap; - } else { - 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 = decode(base64_str) + self.bitmap = bitmap; + } else { + 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)))?; - Some(Bitmap::from_buffer( - image_binary.as_ptr() as *mut u8, - image_binary.len(), - )) + if let Some(base64_str) = data_str.split(',').last() { + let image_binary = decode(base64_str) + .map_err(|e| Error::new(Status::InvalidArg, format!("Decode data url failed {}", e)))?; + Some(Bitmap::from_buffer( + image_binary.as_ptr() as *mut u8, + image_binary.len(), + )) + } else { + None + } } else { - None - } - } else { - Some(Bitmap::from_buffer(src_data.as_ptr() as *mut u8, length)) - }; - if let Some(ref b) = bitmap { - if (image.width - -1.0).abs() < f64::EPSILON { - image.width = b.0.width as f64; - } - if (image.height - -1.0).abs() < f64::EPSILON { - image.height = b.0.height as f64; + Some(Bitmap::from_buffer(data.as_ptr() as *mut u8, length)) + }; + if let Some(ref b) = bitmap { + if (self.width - -1.0).abs() < f64::EPSILON { + self.width = b.0.width as f64; + } + if (self.height - -1.0).abs() < f64::EPSILON { + self.height = b.0.height as f64; + } } + self.bitmap = bitmap + } + self.src = Some(data); + 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))?; } - image.bitmap = bitmap + Ok(()) } - this.set_named_property("_src", src_data.into_raw())?; - 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))?; + pub(crate) fn regenerate_bitmap_if_need(&mut self) { + if !self.need_regenerate_bitmap || !self.is_svg || self.src.is_none() { + return; + } + if let Some(data) = self.src.as_mut() { + 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, + ); + } } - ctx.env.get_undefined() } diff --git a/src/image_pattern.rs b/src/image_pattern.rs deleted file mode 100644 index 11770104..00000000 --- a/src/image_pattern.rs +++ /dev/null @@ -1,147 +0,0 @@ -use std::rc::Rc; - -use napi::*; - -use crate::ctx::{Context, ImageOrCanvas}; -use crate::image::ImageData; -use crate::pattern::Pattern; -use crate::sk::*; - -#[repr(u8)] -enum ImageKind { - ImageData, - Image, - Canvas, -} - -impl From for ImageKind { - fn from(value: u32) -> Self { - match value { - 0 => Self::ImageData, - 1 => Self::Image, - 2 => Self::Canvas, - _ => Self::Image, - } - } -} - -#[js_function(3)] -pub fn canvas_pattern_constructor(ctx: CallContext) -> Result { - let image_or_data = ctx.get::(0)?; - let repetition = ctx.get::(1)?; - let image_kind: ImageKind = ctx.get::(2)?.get_uint32()?.into(); - let mut this: JsObject = ctx.this_unchecked(); - let mut bitmap_to_finalize: Option> = None; - let bitmap = match image_kind { - ImageKind::Image => { - let native_object = ctx - .env - .unwrap::(&image_or_data)? - .get_image() - .unwrap(); - if let Some(bitmap) = native_object.bitmap.as_ref() { - bitmap.0.bitmap - } else { - return Err(Error::new( - Status::GenericFailure, - "Image has not completed".to_string(), - )); - } - } - ImageKind::Canvas => { - let ctx_obj = image_or_data.get_named_property_unchecked::("ctx")?; - let other_ctx = ctx.env.unwrap::(&ctx_obj)?; - bitmap_to_finalize - .insert(Rc::new(other_ctx.surface.get_bitmap())) - .0 - .bitmap - } - ImageKind::ImageData => { - let native_object = ctx.env.unwrap::(&image_or_data)?; - let image_size = native_object.width * native_object.height * 4usize; - let bitmap = Bitmap::from_image_data( - native_object.data as *mut u8, - native_object.width, - native_object.height, - native_object.width * 4usize, - image_size, - ColorType::RGBA8888, - AlphaType::Unpremultiplied, - ); - let bitmap_object = ctx.env.create_external(bitmap, Some(image_size as i64))?; - let bitmap = ctx - .env - .get_value_external::(&bitmap_object)? - .0 - .bitmap; - // wrap Bitmap to `this`, prevent it to be dropped - this.set_named_property("_bitmap", bitmap_object)?; - bitmap - } - }; - let (repeat_x, repeat_y) = match repetition.get_type()? { - ValueType::Null => (TileMode::Repeat, TileMode::Repeat), - ValueType::String => { - let repetition_str = unsafe { repetition.cast::() }.into_utf8()?; - match repetition_str.as_str()? { - "" | "repeat" => (TileMode::Repeat, TileMode::Repeat), - "repeat-x" => (TileMode::Repeat, TileMode::Decal), - "repeat-y" => (TileMode::Decal, TileMode::Repeat), - "no-repeat" => (TileMode::Decal, TileMode::Decal), - _ => { - return Err(Error::new( - Status::InvalidArg, - format!("{} is not valid repetition rule", repetition_str.as_str()?), - )) - } - } - } - _ => { - return Err(Error::new( - Status::InvalidArg, - "Invalid type of image repetition".to_string(), - )) - } - }; - ctx.env.wrap( - &mut this, - Pattern::Image(ImagePattern { - transform: Transform::default(), - bitmap, - repeat_x, - repeat_y, - bitmap_to_finalize, - }), - )?; - ctx.env.get_undefined() -} - -#[js_function(1)] -pub fn set_transform(ctx: CallContext) -> Result { - let this: JsObject = ctx.this_unchecked(); - let transform_object = ctx.get::(0)?; - let a: f64 = transform_object - .get_named_property::("a")? - .get_double()?; - let b: f64 = transform_object - .get_named_property::("b")? - .get_double()?; - let c: f64 = transform_object - .get_named_property::("c")? - .get_double()?; - let d: f64 = transform_object - .get_named_property::("d")? - .get_double()?; - let e: f64 = transform_object - .get_named_property::("e")? - .get_double()?; - let f: f64 = transform_object - .get_named_property::("f")? - .get_double()?; - let transform = Transform::new(a as f32, b as f32, c as f32, d as f32, e as f32, f as f32); - let pattern = ctx.env.unwrap::(&this)?; - if let Pattern::Image(pattern) = pattern { - pattern.transform = transform; - } - ctx.env.get_undefined() -} diff --git a/src/lib.rs b/src/lib.rs index b2e3015f..4c65c044 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,15 +8,19 @@ extern crate napi_derive; #[macro_use] extern crate serde_derive; +use std::str::FromStr; use std::{mem, slice}; -use napi::bindgen_prelude::{AsyncTask, Either3, Object, This, Unknown}; +use napi::bindgen_prelude::{AsyncTask, ClassInstance, Either3, This, Unknown}; use napi::*; -use ctx::{AVIFConfig, Context, ContextData, ContextOutputData}; +use ctx::{ + AVIFConfig, CanvasRenderingContext2D, Context, ContextData, ContextOutputData, SvgExportFlag, + FILL_STYLE_HIDDEN_NAME, STROKE_STYLE_HIDDEN_NAME, +}; use font::{init_font_regexp, FONT_REGEXP}; use rgb::FromSlice; -use sk::SkiaDataRef; +use sk::{ColorSpace, SkiaDataRef}; #[cfg(all( not(all(target_os = "linux", target_env = "musl", target_arch = "aarch64")), @@ -32,14 +36,12 @@ mod font; pub mod global_fonts; mod gradient; mod image; -mod image_pattern; -mod path; +pub mod path; mod pattern; #[allow(dead_code)] mod sk; mod state; pub mod svg; -mod util; const MIME_WEBP: &str = "image/webp"; const MIME_PNG: &str = "image/png"; @@ -54,71 +56,103 @@ const DEFAULT_JPEG_QUALITY: u8 = 92; // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/image-encoders/image_encoder.cc;l=100;drc=81c6f843fdfd8ef660d733289a7a32abe68e247a const DEFAULT_WEBP_QUALITY: u8 = 80; -#[module_exports] -fn init(mut exports: JsObject, env: Env) -> Result<()> { - let canvas_rendering_context2d = ctx::Context::create_js_class(&env)?; - - let image_data_class = image::ImageData::create_js_class(&env)?; - - let image_class = image::Image::create_js_class(&env)?; - - let canvas_pattern = env.define_class( - "CanvasPattern", - image_pattern::canvas_pattern_constructor, - &[Property::new("setTransform")?.with_method(image_pattern::set_transform)], - )?; - - exports.set_named_property("CanvasRenderingContext2D", canvas_rendering_context2d)?; - - exports.set_named_property("ImageData", image_data_class)?; - - exports.set_named_property("Image", image_class)?; - - exports.set_named_property("CanvasPattern", canvas_pattern)?; - +#[napi::module_init] +fn init() { // pre init font regexp FONT_REGEXP.get_or_init(init_font_regexp); - Ok(()) +} + +#[napi(object)] +pub struct CanvasRenderingContext2DAttributes { + pub alpha: Option, + pub color_space: Option, } #[napi] pub struct CanvasElement { pub width: u32, pub height: u32, + pub(crate) ctx: ClassInstance, } #[napi] impl CanvasElement { #[napi(constructor)] - pub fn new(width: u32, height: u32) -> Self { - Self { width, height } + pub fn new(mut env: Env, mut this: This, width: u32, height: u32) -> Result { + let ctx = CanvasRenderingContext2D::into_instance( + CanvasRenderingContext2D { + context: Context::new(width, height, ColorSpace::default())?, + }, + env, + )?; + ctx.as_object(env).define_properties(&[ + Property::new(FILL_STYLE_HIDDEN_NAME)? + .with_value(&env.create_string("#000")?) + .with_property_attributes(PropertyAttributes::Writable | PropertyAttributes::Configurable), + Property::new(STROKE_STYLE_HIDDEN_NAME)? + .with_value(&env.create_string("#000")?) + .with_property_attributes(PropertyAttributes::Writable | PropertyAttributes::Configurable), + ])?; + env.adjust_external_memory((width * height * 4) as i64)?; + this.define_properties(&[Property::new("ctx")? + .with_value(&ctx) + .with_property_attributes(PropertyAttributes::Default)])?; + Ok(Self { width, height, ctx }) + } + + #[napi] + pub fn get_context( + &mut self, + this: This, + context_type: String, + attrs: Option, + ) -> Result { + if context_type != "2d" { + return Err(Error::new( + Status::InvalidArg, + format!("{context_type} is not supported"), + )); + } + let context_2d = &mut self.ctx.context; + if !attrs.as_ref().and_then(|a| a.alpha).unwrap_or(true) { + let mut fill_paint = context_2d.fill_paint()?; + fill_paint.set_color(255, 255, 255, 255); + context_2d.alpha = false; + context_2d.surface.draw_rect( + 0f32, + 0f32, + self.width as f32, + self.height as f32, + &fill_paint, + ); + } + let color_space = attrs + .and_then(|a| a.color_space) + .and_then(|cs| ColorSpace::from_str(&cs).ok()) + .unwrap_or_default(); + context_2d.color_space = color_space; + this.get_named_property("ctx") } #[napi] pub fn encode( &self, - env: Env, - this: This, format: String, quality_or_config: Either3, ) -> Result> { - Ok(AsyncTask::new(self.encode_inner( - env, - this, - format, - quality_or_config, - )?)) + Ok(AsyncTask::new( + self.encode_inner(format, quality_or_config)?, + )) } #[napi] pub fn encode_sync( &self, env: Env, - this: This, format: String, quality_or_config: Either3, ) -> Result { - let mut task = self.encode_inner(env, this, format, quality_or_config)?; + let mut task = self.encode_inner(format, quality_or_config)?; let output = task.compute()?; task.resolve(env, output) } @@ -127,12 +161,11 @@ impl CanvasElement { pub fn to_buffer( &self, env: Env, - this: This, mime: String, quality_or_config: Either3, ) -> Result { let mime = mime.as_str(); - let context_data = get_data_ref(&env, &this, mime, &quality_or_config)?; + let context_data = get_data_ref(&self.ctx.context, mime, &quality_or_config)?; match context_data { ContextOutputData::Skia(data_ref) => unsafe { env @@ -149,9 +182,8 @@ impl CanvasElement { } #[napi] - pub fn data(&self, env: Env, this: This) -> Result { - let ctx_js = this.get_named_property::("ctx")?; - let ctx2d = env.unwrap::(&ctx_js)?; + pub fn data(&self, env: Env) -> Result { + let ctx2d = &self.ctx.context; let surface_ref = ctx2d.surface.reference(); @@ -171,51 +203,38 @@ impl CanvasElement { #[napi(js_name = "toDataURLAsync")] pub fn to_data_url_async( &self, - env: Env, - this: This, mime: String, quality_or_config: Either3, ) -> Result> { - Ok(AsyncTask::new(self.to_data_url_inner( - &env, - &this, - mime.as_str(), - quality_or_config, - )?)) + Ok(AsyncTask::new( + self.to_data_url_inner(mime.as_str(), quality_or_config)?, + )) } #[napi(js_name = "toDataURL")] pub fn to_data_url( &self, - env: Env, - this: This, mime: String, quality_or_config: Either3, ) -> Result { - let mut task = self.to_data_url_inner(&env, &this, mime.as_str(), quality_or_config)?; + let mut task = self.to_data_url_inner(mime.as_str(), quality_or_config)?; task.compute() } #[napi] - pub fn save_png(&self, env: Env, this: This, path: String) -> Result<()> { - let ctx_js = this.get_named_property::("ctx")?; - let ctx2d = env.unwrap::(&ctx_js)?; - + pub fn save_png(&self, path: String) { + let ctx2d = &self.ctx.context; ctx2d.surface.save_png(&path); - Ok(()) } fn encode_inner( &self, - env: Env, - this: This, format: String, quality_or_config: Either3, ) -> Result { let format_str = format.as_str(); let quality = quality_or_config.to_quality(format_str); - let ctx_js = this.get_named_property::("ctx")?; - let ctx2d = env.unwrap::(&ctx_js)?; + let ctx2d = &self.ctx.context; let surface_ref = ctx2d.surface.reference(); let task = match format_str { @@ -239,14 +258,11 @@ impl CanvasElement { fn to_data_url_inner( &self, - env: &Env, - this: &This, mime: &str, quality_or_config: Either3, ) -> Result { let data_ref = get_data_ref( - env, - this, + &self.ctx.context, mime, &match quality_or_config { Either3::A(q) => Either3::A((q * 100.0) as u32), @@ -266,34 +282,11 @@ pub struct ContextAttr { pub alpha: Option, } -#[napi] -pub fn create_context( - env: Env, - context_object: JsObject, - width: f64, - height: f64, - attrs: ContextAttr, -) -> Result<()> { - let context_2d: &mut Context = env.unwrap(&context_object)?; - if !attrs.alpha.unwrap_or(true) { - let mut fill_paint = context_2d.fill_paint()?; - fill_paint.set_color(255, 255, 255, 255); - context_2d.alpha = false; - context_2d - .surface - .draw_rect(0f32, 0f32, width as f32, height as f32, &fill_paint); - } - Ok(()) -} - fn get_data_ref( - env: &Env, - this: &Object, + ctx2d: &Context, mime: &str, quality_or_config: &Either3, ) -> Result { - let ctx_js = this.get_named_property::("ctx")?; - let ctx2d = env.unwrap::(&ctx_js)?; let surface_ref = ctx2d.surface.reference(); let quality = quality_or_config.to_quality(mime); @@ -392,22 +385,78 @@ impl ToQuality for Either3 { pub struct SVGCanvas { pub width: u32, pub height: u32, + pub(crate) ctx: ClassInstance, } #[napi] impl SVGCanvas { #[napi(constructor)] - pub fn new(width: u32, height: u32) -> Self { - Self { width, height } + pub fn new( + mut env: Env, + mut this: This, + width: u32, + height: u32, + flag: SvgExportFlag, + ) -> Result { + let ctx = CanvasRenderingContext2D::into_instance( + CanvasRenderingContext2D { + context: Context::new_svg(width, height, flag.into(), ColorSpace::default())?, + }, + env, + )?; + ctx.as_object(env).define_properties(&[ + Property::new(FILL_STYLE_HIDDEN_NAME)? + .with_value(&env.create_string("#000")?) + .with_property_attributes(PropertyAttributes::Writable | PropertyAttributes::Configurable), + Property::new(STROKE_STYLE_HIDDEN_NAME)? + .with_value(&env.create_string("#000")?) + .with_property_attributes(PropertyAttributes::Writable | PropertyAttributes::Configurable), + ])?; + env.adjust_external_memory((width * height * 4) as i64)?; + this.define_properties(&[Property::new("ctx")? + .with_value(&ctx) + .with_property_attributes(PropertyAttributes::Default)])?; + Ok(Self { width, height, ctx }) } #[napi] - pub fn get_content(&self, this: This, env: Env) -> Result { - let ctx_js = this.get_named_property::("ctx")?; - let ctx2d = env.unwrap::(&ctx_js)?; + pub fn get_context( + &mut self, + this: This, + context_type: String, + attrs: Option, + ) -> Result { + if context_type != "2d" { + return Err(Error::new( + Status::InvalidArg, + format!("{context_type} is not supported"), + )); + } + let context_2d = &mut self.ctx.context; + if !attrs.as_ref().and_then(|a| a.alpha).unwrap_or(true) { + let mut fill_paint = context_2d.fill_paint()?; + fill_paint.set_color(255, 255, 255, 255); + context_2d.alpha = false; + context_2d.surface.draw_rect( + 0f32, + 0f32, + self.width as f32, + self.height as f32, + &fill_paint, + ); + } + let color_space = attrs + .and_then(|a| a.color_space) + .and_then(|cs| ColorSpace::from_str(&cs).ok()) + .unwrap_or_default(); + context_2d.color_space = color_space; + this.get_named_property("ctx") + } - let svg_data_stream = ctx2d.stream.as_ref().unwrap(); - let svg_data = svg_data_stream.data(ctx2d.width, ctx2d.height); + #[napi] + pub fn get_content(&self, env: Env) -> Result { + let svg_data_stream = self.ctx.context.stream.as_ref().unwrap(); + let svg_data = svg_data_stream.data(self.ctx.context.width, self.ctx.context.height); unsafe { env .create_buffer_with_borrowed_data(svg_data.0.ptr, svg_data.0.size, svg_data, |d, _| { diff --git a/src/path.rs b/src/path.rs index 36409871..4141717a 100644 --- a/src/path.rs +++ b/src/path.rs @@ -354,4 +354,13 @@ impl Path { pub fn equals(&self, other: &Path) -> bool { self.inner == other.inner } + + #[napi] + pub fn is_point_in_path(&self, x: f64, y: f64, fill_type: Option) -> bool { + self.inner.hit_test( + x as f32, + y as f32, + fill_type.unwrap_or(FillType::Winding).into(), + ) + } } diff --git a/src/pattern.rs b/src/pattern.rs index 4d0b98ad..fe62c0df 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -1,13 +1,19 @@ +use std::result::Result as StdResult; + use cssparser::{Color as CSSColor, Parser, ParserInput, RGBA}; +use napi::bindgen_prelude::*; +use crate::ctx::TransformObject; use crate::error::SkError; -use crate::gradient::CanvasGradient; -use crate::sk::ImagePattern; +use crate::gradient::Gradient; +use crate::image::{Image, ImageData}; +use crate::sk::{AlphaType, Bitmap, ColorType, ImagePattern, TileMode, Transform}; +use crate::{CanvasElement, SVGCanvas}; #[derive(Debug, Clone)] pub enum Pattern { Color(RGBA, String), - Gradient(CanvasGradient), + Gradient(Gradient), Image(ImagePattern), } @@ -18,7 +24,7 @@ impl Default for Pattern { } impl Pattern { - pub fn from_color(color_str: &str) -> Result { + pub fn from_color(color_str: &str) -> StdResult { let mut parser_input = ParserInput::new(color_str); let mut parser = Parser::new(&mut parser_input); let color = CSSColor::parse(&mut parser) @@ -31,3 +37,87 @@ impl Pattern { } } } + +#[napi] +pub struct CanvasPattern { + pub(crate) inner: Pattern, + #[allow(unused)] + // hold it for Drop + bitmap: Option, +} + +#[napi] +impl CanvasPattern { + #[napi(constructor)] + pub fn new( + input: Either4<&mut Image, &mut ImageData, &mut CanvasElement, &mut SVGCanvas>, + repetition: Option, + ) -> Result { + let mut inner_bitmap = None; + let bitmap = match input { + Either4::A(image) => image + .bitmap + .as_mut() + .map(|b| b.0.bitmap) + .ok_or_else(|| Error::new(Status::InvalidArg, "Image is not completed.".to_owned()))?, + Either4::B(image_data) => { + let image_data_size = image_data.width * image_data.height * 4; + let bitmap = Bitmap::from_image_data( + image_data.data, + image_data.width, + image_data.height, + image_data.width * 4, + image_data_size, + ColorType::RGBA8888, + AlphaType::Unpremultiplied, + ); + let ptr = bitmap.0.bitmap; + inner_bitmap = Some(bitmap); + ptr + } + Either4::C(canvas) => { + let canvas_bitmap = canvas.ctx.context.surface.get_bitmap(); + let ptr = canvas_bitmap.0.bitmap; + inner_bitmap = Some(canvas_bitmap); + ptr + } + Either4::D(svg_canvas) => { + let canvas_bitmap = svg_canvas.ctx.context.surface.get_bitmap(); + let ptr = canvas_bitmap.0.bitmap; + inner_bitmap = Some(canvas_bitmap); + ptr + } + }; + let (repeat_x, repeat_y) = match repetition { + None => (TileMode::Repeat, TileMode::Repeat), + Some(repetition) => match repetition.as_str() { + "" | "repeat" => (TileMode::Repeat, TileMode::Repeat), + "repeat-x" => (TileMode::Repeat, TileMode::Decal), + "repeat-y" => (TileMode::Decal, TileMode::Repeat), + "no-repeat" => (TileMode::Decal, TileMode::Decal), + _ => { + return Err(Error::new( + Status::InvalidArg, + format!("{repetition} is not valid repetition rule"), + )) + } + }, + }; + Ok(Self { + inner: Pattern::Image(ImagePattern { + transform: Transform::default(), + bitmap, + repeat_x, + repeat_y, + }), + bitmap: inner_bitmap, + }) + } + + #[napi] + pub fn set_transform(&mut self, transform: TransformObject) { + if let Pattern::Image(image) = &mut self.inner { + image.transform = transform.into(); + } + } +} diff --git a/src/sk.rs b/src/sk.rs index 712f67ee..0116cc87 100644 --- a/src/sk.rs +++ b/src/sk.rs @@ -5,7 +5,6 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use std::os::raw::c_char; use std::ptr; -use std::rc::Rc; use std::slice; use std::str::FromStr; @@ -3432,7 +3431,6 @@ pub struct ImagePattern { pub(crate) repeat_x: TileMode, pub(crate) repeat_y: TileMode, pub(crate) transform: Transform, - pub(crate) bitmap_to_finalize: Option>, } impl ImagePattern { diff --git a/src/state.rs b/src/state.rs index 10226d68..d5a5f371 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,7 +5,7 @@ use crate::sk::{ImageFilter, Matrix}; use super::{ font::Font, pattern::Pattern, - sk::{BlendMode, FilterQuality, Paint, TextAlign, TextBaseline, TextDirection}, + sk::{FilterQuality, Paint, TextAlign, TextBaseline, TextDirection}, }; #[derive(Debug, Clone)] @@ -20,7 +20,6 @@ pub struct Context2dRenderingState { pub shadow_color_string: String, pub global_alpha: f32, pub line_dash_offset: f32, - pub global_composite_operation: BlendMode, pub image_smoothing_enabled: bool, pub image_smoothing_quality: FilterQuality, pub paint: Paint, @@ -49,7 +48,6 @@ impl Default for Context2dRenderingState { global_alpha: 1.0, /// A float specifying the amount of the line dash offset. The default value is 0.0. line_dash_offset: 0.0, - global_composite_operation: BlendMode::default(), image_smoothing_enabled: true, image_smoothing_quality: FilterQuality::default(), paint: Paint::default(), diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 9d566be0..00000000 --- a/src/util.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::any::type_name; - -use napi::{bindgen_prelude::ValidateNapiValue, Env, JsObject, Result}; - -pub trait UnwrapObject { - fn unwrap(&self, env: &Env) -> Result<&mut Target> - where - &'static Target: ValidateNapiValue + 'static; -} - -impl UnwrapObject for JsObject { - fn unwrap(&self, env: &Env) -> Result<&mut Target> - where - &'static Target: ValidateNapiValue + 'static, - { - use napi::NapiRaw; - - unsafe { <&'static Target>::validate(env.raw(), self.raw()) }.and_then(|_| { - let mut path_ptr = std::ptr::null_mut(); - napi::check_status!( - unsafe { napi::sys::napi_unwrap(env.raw(), self.raw(), &mut path_ptr) }, - "Unwrap Path from {} failed", - type_name::() - )?; - Ok(Box::leak(unsafe { Box::from_raw(path_ptr as *mut Target) })) - }) - } -}