From f443c4f64c27c4fc40ca392b04a6ebdd754580b8 Mon Sep 17 00:00:00 2001 From: Zimond Date: Mon, 18 Sep 2023 12:51:28 +0800 Subject: [PATCH] feat: upgrade to usvg/resvg 0.34 --- Cargo.toml | 13 ++-- __test__/index.spec.ts | 6 +- src/fonts.rs | 8 +-- src/lib.rs | 153 ++++++++++++++++++----------------------- src/options.rs | 54 ++++++++++----- 5 files changed, 117 insertions(+), 117 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ada85b5..5bbbd614 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ pathfinder_geometry = "0.5.1" pathfinder_content = { version = "0.5.0", default-features = false } pathfinder_simd = { version = "0.5.1", features = ["pf-no-simd"] } futures = "0.3.21" -usvg-writer = { git = "https://github.com/zimond/resvg", rev = "6201182c" } [target.'cfg(all(not(all(target_os = "linux", target_arch = "aarch64", target_env = "musl")), not(all(target_os = "windows", target_arch = "aarch64")), not(target_arch = "wasm32")))'.dependencies] mimalloc-rust = { version = "0.2" } @@ -29,17 +28,15 @@ mimalloc-rust = { version = "0.2" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2.84" js-sys = "0.3.61" -resvg = { version = "0.29.0", default-features = false, features = [ - "filter", +resvg = { version = "0.34.0", default-features = false, features = [ "raster-images", "text", ] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -napi = { version = "2.12.1", features = ["serde-json", "async"] } -napi-derive = "2.10.1" -resvg = { version = "0.29.0", default-features = false, features = [ - "filter", +napi = { version = "2.13.3", features = ["serde-json", "async"] } +napi-derive = "2.13.0" +resvg = { version = "0.34.0", default-features = false, features = [ "raster-images", "text", "system-fonts", @@ -56,4 +53,4 @@ opt-level = 3 codegen-units = 1 [patch.crates-io] -resvg = { git = "https://github.com/zimond/resvg", rev = "6201182c" } +resvg = { git = "https://github.com/zimond/resvg", rev = "9a44a3c" } diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 6ecba479..fd15a8d4 100755 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -243,7 +243,7 @@ test('should be load custom fontFiles(no defaultFontFamily option)', (t) => { const originPixels = pngData.pixels.toJSON().data // Find the number of blue `rgb(0,255,255)`pixels - t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) + t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1727) }) test('should be load custom fontDirs(no defaultFontFamily option)', (t) => { @@ -265,7 +265,7 @@ test('should be load custom fontDirs(no defaultFontFamily option)', (t) => { const originPixels = pngData.pixels.toJSON().data // Find the number of blue `rgb(0,255,255)`pixels - t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) + t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1727) }) test('The defaultFontFamily is not found in the OS and needs to be fallback', (t) => { @@ -521,7 +521,7 @@ test('should get svg bbox(rect)', async (t) => { const result = await jimp.read(pngBuffer) t.is(bbox.width, 200) - t.is(bbox.height, 100.00000000000001) + t.is(bbox.height, 100) t.is(bbox.x, 50.4) t.is(bbox.y, 60.8) diff --git a/src/fonts.rs b/src/fonts.rs index 90c71c36..9bf6d747 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -3,13 +3,13 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use crate::options::*; -use resvg::usvg_text_layout::fontdb::{Database, Language}; +use resvg::usvg::fontdb::{Database, Language}; #[cfg(not(target_arch = "wasm32"))] use log::{debug, warn}; #[cfg(not(target_arch = "wasm32"))] -use resvg::usvg_text_layout::fontdb::{Family, Query, Source}; +use resvg::usvg::fontdb::{Family, Query, Source}; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsCast; @@ -86,7 +86,6 @@ fn set_font_families(font_options: &JsFontOptions, fontdb: &mut Database) { let fontdb_found_default_font_family = fontdb .faces() - .iter() .find_map(|it| { it.families .iter() @@ -126,7 +125,6 @@ fn set_wasm_font_families( let fontdb_found_default_font_family = fontdb .faces() - .iter() .find_map(|it| { it.families .iter() @@ -194,7 +192,7 @@ fn find_and_debug_font_path(fontdb: &mut Database, font_family: &str) { fn get_first_font_family_or_fallback(fontdb: &mut Database) -> String { let mut default_font_family = "Arial".to_string(); // 其他情况都 fallback 到指定的这个字体。 - match fontdb.faces().iter().next() { + match fontdb.faces().next() { Some(face) => { let base_family = face .families diff --git a/src/lib.rs b/src/lib.rs index 73c339eb..a0feeea3 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,9 +18,8 @@ use pathfinder_content::{ use pathfinder_geometry::rect::RectF; use pathfinder_geometry::vector::Vector2F; use resvg::{ - tiny_skia::Pixmap, - usvg::{self, ImageKind, NodeKind}, - usvg_text_layout::TreeTextToPath, + tiny_skia::{PathSegment, Pixmap, Point}, + usvg::{self, ImageKind, NodeKind, TreeParsing, TreeTextToPath}, }; #[cfg(target_arch = "wasm32")] use wasm_bindgen::{ @@ -178,7 +177,8 @@ impl Resvg { #[napi] /// Output usvg-simplified SVG string pub fn to_string(&self) -> String { - usvg_writer::export_tree_to_string(&self.tree, &usvg_writer::XmlOptions::default()) + use usvg::TreeWriting; + self.tree.to_string(&usvg::XmlOptions::default()) } #[napi(js_name = innerBBox)] @@ -191,8 +191,8 @@ impl Resvg { pub fn inner_bbox(&self) -> Either { let rect = self.tree.view_box.rect; let rect = points_to_rect( - usvg::Point::new(rect.x(), rect.y()), - usvg::Point::new(rect.right(), rect.bottom()), + Vector2F::new(rect.x(), rect.y()), + Vector2F::new(rect.right(), rect.bottom()), ); let mut v = None; for child in self.tree.root.children() { @@ -227,10 +227,10 @@ impl Resvg { pub fn get_bbox(&self) -> Either { match self.tree.root.calculate_bbox() { Some(bbox) => Either::A(BBox { - x: bbox.x(), - y: bbox.y(), - width: bbox.width(), - height: bbox.height(), + x: (bbox.x() * 100.0).round() as f64 / 100.0, + y: (bbox.y() * 100.0).round() as f64 / 100.0, + width: (bbox.width() * 100.0).round() as f64 / 100.0, + height: (bbox.height() * 100.0).round() as f64 / 100.0, }), None => Either::B(()), } @@ -243,10 +243,11 @@ impl Resvg { if !bbox.width.is_finite() || !bbox.height.is_finite() { return; } - let width = bbox.width; - let height = bbox.height; - self.tree.view_box.rect = usvg::Rect::new(bbox.x, bbox.y, width, height).unwrap(); - self.tree.size = usvg::Size::new(width, height).unwrap(); + let width = bbox.width as f32; + let height = bbox.height as f32; + self.tree.view_box.rect = + usvg::NonZeroRect::from_xywh(bbox.x as f32, bbox.y as f32, width, height).unwrap(); + self.tree.size = usvg::Size::from_wh(width, height).unwrap(); } #[napi] @@ -262,13 +263,13 @@ impl Resvg { /// Get the SVG width #[napi(getter)] - pub fn width(&self) -> f64 { + pub fn width(&self) -> f32 { self.tree.size.width().round() } /// Get the SVG height #[napi(getter)] - pub fn height(&self) -> f64 { + pub fn height(&self) -> f32 { self.tree.size.height().round() } } @@ -306,13 +307,13 @@ impl Resvg { /// Get the SVG width #[wasm_bindgen(getter)] - pub fn width(&self) -> f64 { + pub fn width(&self) -> f32 { self.tree.size.width().round() } /// Get the SVG height #[wasm_bindgen(getter)] - pub fn height(&self) -> f64 { + pub fn height(&self) -> f32 { self.tree.size.height().round() } @@ -324,7 +325,8 @@ impl Resvg { /// Output usvg-simplified SVG string #[wasm_bindgen(js_name = toString)] pub fn to_string(&self) -> String { - usvg_writer::export_tree_to_string(&self.tree, &usvg_writer::XmlOptions::default()) + use usvg::TreeWriting; + self.tree.to_string(&usvg::XmlOptions::default()) } /// Calculate a maximum bounding box of all visible elements in this SVG. @@ -334,8 +336,8 @@ impl Resvg { pub fn inner_bbox(&self) -> Option { let rect = self.tree.view_box.rect; let rect = points_to_rect( - usvg::Point::new(rect.x(), rect.y()), - usvg::Point::new(rect.right(), rect.bottom()), + Vector2F::new(rect.x(), rect.y()), + Vector2F::new(rect.right(), rect.bottom()), ); let mut v = None; for child in self.tree.root.children() { @@ -365,10 +367,10 @@ impl Resvg { pub fn get_bbox(&self) -> Option { let bbox = self.tree.root.calculate_bbox()?; Some(BBox { - x: bbox.x(), - y: bbox.y(), - width: bbox.width(), - height: bbox.height(), + x: bbox.x() as f64, + y: bbox.y() as f64, + width: bbox.width() as f64, + height: bbox.height() as f64, }) } @@ -379,10 +381,11 @@ impl Resvg { if !bbox.width.is_finite() || !bbox.height.is_finite() { return; } - let width = bbox.width; - let height = bbox.height; - self.tree.view_box.rect = usvg::Rect::new(bbox.x, bbox.y, width, height).unwrap(); - self.tree.size = usvg::Size::new(width, height).unwrap(); + let width = bbox.width as f32; + let height = bbox.height as f32; + self.tree.view_box.rect = + usvg::NonZeroRect::from_xywh(bbox.x as f32, bbox.y as f32, width, height).unwrap(); + self.tree.size = usvg::Size::from_wh(width, height).unwrap(); } #[wasm_bindgen(js_name = imagesToResolve)] @@ -426,16 +429,16 @@ impl Resvg { let mut iter = p.data.segments().peekable(); while let Some(seg) = iter.next() { match seg { - usvg::PathSegment::MoveTo { x, y } => { + PathSegment::MoveTo(p) => { if !contour.is_empty() { outline .push_contour(std::mem::replace(&mut contour, Contour::new())); } - contour.push_endpoint(Vector2F::new(x as f32, y as f32)); + contour.push_endpoint(Vector2F::new(p.x, p.y)) } - usvg::PathSegment::LineTo { x, y } => { - let v = Vector2F::new(x as f32, y as f32); - if let Some(usvg::PathSegment::ClosePath) = iter.peek() { + PathSegment::LineTo(p) => { + let v = Vector2F::new(p.x, p.y); + if let Some(PathSegment::Close) = iter.peek() { let first = contour.position_of(0); if (first - v).square_length() < 1.0 { continue; @@ -443,21 +446,18 @@ impl Resvg { } contour.push_endpoint(v); } - usvg::PathSegment::CurveTo { - x1, - y1, - x2, - y2, - x, - y, - } => { + PathSegment::CubicTo(p1, p2, p) => { contour.push_cubic( - Vector2F::new(x1 as f32, y1 as f32), - Vector2F::new(x2 as f32, y2 as f32), - Vector2F::new(x as f32, y as f32), + Vector2F::new(p1.x, p1.y), + Vector2F::new(p2.x, p2.y), + Vector2F::new(p.x, p.y), ); } - usvg::PathSegment::ClosePath => { + PathSegment::QuadTo(p1, p) => { + contour + .push_quadratic(Vector2F::new(p1.x, p1.y), Vector2F::new(p.x, p.y)); + } + PathSegment::Close => { contour.close(); outline.push_contour(std::mem::replace(&mut contour, Contour::new())); } @@ -511,24 +511,24 @@ impl Resvg { usvg::NodeKind::Image(image) => { let rect = image.view_box.rect; Some(points_to_rect( - usvg::Point::new(rect.x(), rect.y()), - usvg::Point::new(rect.right(), rect.bottom()), + Vector2F::new(rect.x(), rect.y()), + Vector2F::new(rect.right(), rect.bottom()), )) } usvg::NodeKind::Text(_) => None, }?; - let (x1, y1) = transform.apply(bbox.min_x() as f64, bbox.min_y() as f64); - let (x2, y2) = transform.apply(bbox.max_x() as f64, bbox.max_y() as f64); - let (x3, y3) = transform.apply(bbox.min_x() as f64, bbox.max_y() as f64); - let (x4, y4) = transform.apply(bbox.max_x() as f64, bbox.min_y() as f64); - let x_min = x1.min(x2).min(x3).min(x4); - let x_max = x1.max(x2).max(x3).max(x4); - let y_min = y1.min(y2).min(y3).min(y4); - let y_max = y1.max(y2).max(y3).max(y4); - let r = points_to_rect( - usvg::Point::new(x_min, y_min), - usvg::Point::new(x_max, y_max), - ); + let mut pts = vec![ + Point::from_xy(bbox.min_x(), bbox.min_y()), + Point::from_xy(bbox.max_x(), bbox.max_y()), + Point::from_xy(bbox.min_x(), bbox.max_y()), + Point::from_xy(bbox.max_x(), bbox.min_y()), + ]; + transform.map_points(&mut pts); + let x_min = pts[0].x.min(pts[1].x).min(pts[2].x).min(pts[3].x); + let x_max = pts[0].x.max(pts[1].x).max(pts[2].x).max(pts[3].x); + let y_min = pts[0].y.min(pts[1].y).min(pts[2].y).min(pts[3].y); + let y_max = pts[0].y.max(pts[1].y).max(pts[2].y).max(pts[3].y); + let r = points_to_rect(Vector2F::new(x_min, y_min), Vector2F::new(x_max, y_max)); Some(r) } @@ -540,32 +540,17 @@ impl Resvg { } fn render_inner(&self) -> Result { - let pixmap_size = self - .js_options - .fit_to - .fit_to(self.tree.size.to_screen_size()) - .ok_or_else(|| Error::ZeroSized)?; - let mut pixmap = self.js_options.create_pixmap(pixmap_size)?; + let (width, height, transform) = self.js_options.fit_to.fit_to(self.tree.size)?; + let mut pixmap = self.js_options.create_pixmap(width, height)?; // Render the tree - let _image = resvg::render( - &self.tree, - self.js_options.fit_to, - resvg::tiny_skia::Transform::default(), - pixmap.as_mut(), - ); + let _image = resvg::Tree::from_usvg(&self.tree).render(transform, &mut pixmap.as_mut()); // Crop the SVG let crop_rect = resvg::tiny_skia::IntRect::from_ltrb( self.js_options.crop.left, self.js_options.crop.top, - self.js_options - .crop - .right - .unwrap_or(pixmap_size.width() as i32), - self.js_options - .crop - .bottom - .unwrap_or(pixmap_size.height() as i32), + self.js_options.crop.right.unwrap_or(width as i32), + self.js_options.crop.bottom.unwrap_or(height as i32), ); if let Some(crop_rect) = crop_rect { @@ -580,7 +565,7 @@ impl Resvg { for node in self.tree.root.descendants() { if let NodeKind::Image(i) = &mut *node.borrow_mut() { if let ImageKind::RAW(_, _, buffer) = &mut i.kind { - let s = String::from_utf8(buffer.clone())?; + let s = String::from_utf8(buffer.as_slice().to_vec())?; data.push(s); } } @@ -596,7 +581,7 @@ impl Resvg { for node in self.tree.root.descendants() { if let NodeKind::Image(i) = &mut *node.borrow_mut() { let matched = if let ImageKind::RAW(_, _, data) = &mut i.kind { - let s = String::from_utf8(data.clone()).map_err(Error::from)?; + let s = String::from_utf8(data.as_slice().to_vec()).map_err(Error::from)?; s == href } else { false @@ -652,9 +637,7 @@ pub fn render_async( } } -fn points_to_rect(min: usvg::Point, max: usvg::Point) -> RectF { - let min = Vector2F::new(min.x as f32, min.y as f32); - let max = Vector2F::new(max.x as f32, max.y as f32); +fn points_to_rect(min: Vector2F, max: Vector2F) -> RectF { RectF::new(min, max - min) } diff --git a/src/options.rs b/src/options.rs index cbe31801..e42d7d71 100644 --- a/src/options.rs +++ b/src/options.rs @@ -7,10 +7,9 @@ use std::sync::Arc; use crate::error::Error; #[cfg(not(target_arch = "wasm32"))] use napi::{bindgen_prelude::Buffer, Either}; -use resvg::tiny_skia::Pixmap; -use resvg::usvg::{self, ImageHrefResolver, ImageKind}; -use resvg::usvg::{Options, ScreenSize}; -use resvg::usvg_text_layout::fontdb::Database; +use resvg::tiny_skia::{Pixmap, Transform}; +use resvg::usvg::fontdb::Database; +use resvg::usvg::{self, ImageHrefResolver, ImageKind, Options, TreeParsing}; use serde::{Deserialize, Deserializer}; /// Image fit options. @@ -20,10 +19,9 @@ use serde::{Deserialize, Deserializer}; tag = "mode", content = "value", rename_all = "lowercase", - deny_unknown_fields, - remote = "usvg::FitTo" + deny_unknown_fields )] -enum FitToDef { +pub enum FitToDef { /// Keep original size. Original, /// Scale to width. @@ -34,6 +32,31 @@ enum FitToDef { Zoom(f32), } +impl FitToDef { + pub(crate) fn fit_to(&self, size: usvg::Size) -> Result<(u32, u32, Transform), Error> { + let mut transform = Transform::identity(); + let width = size.width(); + let height = size.height(); + let scale = match self { + FitToDef::Original => 1.0, + FitToDef::Width(w) => *w as f32 / width, + FitToDef::Height(h) => *h as f32 / height, + FitToDef::Zoom(s) => *s, + }; + let width = (width * scale).round().max(0.0) as u32; + let height = (height * scale).round().max(0.0) as u32; + transform = transform.pre_scale( + width as f32 / size.width() as f32, + height as f32 / size.height() as f32, + ); + if width == 0 || height == 0 { + Err(Error::ZeroSized) + } else { + Ok((width, height, transform)) + } + } +} + #[derive(Deserialize)] #[serde(rename_all = "lowercase", remote = "log::LevelFilter")] enum LogLevelDef { @@ -88,7 +111,7 @@ pub struct JsOptions { /// https://github.com/RazrFalcon/resvg/issues/526#issuecomment-1190433890 /// /// Default: 96.0 - pub dpi: f64, + pub dpi: f32, /// A list of languages. /// @@ -126,8 +149,7 @@ pub struct JsOptions { /// The size to render the SVG. /// /// Default: Original - #[serde(with = "FitToDef")] - pub fit_to: usvg::FitTo, + pub fit_to: FitToDef, /// The background color of the SVG. /// @@ -150,7 +172,7 @@ impl Default for JsOptions { shape_rendering: usvg::ShapeRendering::default(), text_rendering: usvg::TextRendering::default(), image_rendering: usvg::ImageRendering::default(), - fit_to: usvg::FitTo::Original, + fit_to: FitToDef::Original, background: None, crop: JsCropOptions::default(), log_level: log::LevelFilter::Error, @@ -176,13 +198,13 @@ impl JsOptions { shape_rendering: self.shape_rendering, text_rendering: self.text_rendering, image_rendering: self.image_rendering, - default_size: usvg::Size::new(100.0_f64, 100.0_f64).unwrap(), + default_size: usvg::Size::from_wh(100.0, 100.0).unwrap(), image_href_resolver: usvg::ImageHrefResolver::default(), }; (opts, fontdb) } - pub(crate) fn create_pixmap(&self, size: ScreenSize) -> Result { + pub(crate) fn create_pixmap(&self, width: u32, height: u32) -> Result { // Parse the background let background = self .background @@ -191,7 +213,7 @@ impl JsOptions { .transpose()?; // Unwrap is safe, because `size` is already valid. - let mut pixmap = Pixmap::new(size.width(), size.height()).unwrap(); + let mut pixmap = Pixmap::new(width, height).unwrap(); if let Some(bg) = background { let color = resvg::tiny_skia::Color::from_rgba8(bg.red, bg.green, bg.blue, bg.alpha); @@ -228,7 +250,7 @@ pub struct JsFontOptions { /// Will be used when no `font-size` attribute is set in the SVG. /// /// Default: 12 - pub default_font_size: f64, + pub default_font_size: f32, /// The 'serif' font family. /// @@ -346,7 +368,7 @@ pub(crate) fn tweak_usvg_options(opts: &mut usvg::Options) { opts.image_href_resolver = ImageHrefResolver::default(); opts.image_href_resolver.resolve_string = Arc::new(move |data: &str, opts: &Options| { if data.starts_with("https://") || data.starts_with("http://") { - Some(ImageKind::RAW(1, 1, data.as_bytes().to_vec())) + Some(ImageKind::RAW(1, 1, Arc::new(data.as_bytes().to_vec()))) } else { let resolver = ImageHrefResolver::default().resolve_string; (resolver)(data, opts)