Skip to content

Commit

Permalink
feat: add font resolve support and builder api
Browse files Browse the repository at this point in the history
  • Loading branch information
zimond committed Jul 15, 2022
1 parent 4639aa4 commit 5f96aa8
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 61 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pathfinder_content = { version = "0.5.0", default-features = false }
pathfinder_simd = { version = "0.5.1", features = ["pf-no-simd"] }
futures = "0.3.21"
infer = "0.9.0"
ouroboros = "0.15.0"
roxmltree = "0.14.1"
fontkit = "0.1.0"

[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" }
Expand All @@ -38,6 +41,7 @@ js-sys = "0.3.58"
usvg = { version = "0.22.0", default-features = false, features = [
"export",
"filter",
"text"
] }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
Expand Down
201 changes: 201 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use fontdb::{Database, Weight};
use fontkit::{FontKey, Width};
#[cfg(not(target_arch = "wasm32"))]
use napi::bindgen_prelude::{Buffer, Either, Error as NapiError};
#[cfg(not(target_arch = "wasm32"))]
use napi_derive::napi;
use ouroboros::self_referencing;
use roxmltree::Document;

use crate::{options::JsOptions, Resvg};

#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
#[cfg_attr(not(target_arch = "wasm32"), napi)]
#[ouroboros::self_referencing]
pub struct ResvgBuilder {
js_options: JsOptions,
data: String,
#[borrows(data)]
#[covariant]
doc: Document<'this>,
}

#[napi(js_name = "FontKey")]
pub struct FontKeyWrapper(FontKey);

#[cfg(not(target_arch = "wasm32"))]
#[napi]
impl ResvgBuilder {
#[napi(constructor)]
pub fn new_napi(
svg: Either<String, Buffer>,
options: Option<String>,
) -> Result<ResvgBuilder, NapiError> {
ResvgBuilder::new_napi_inner(&svg, options)
}

pub fn new_napi_inner(
svg: &Either<String, Buffer>,
options: Option<String>,
) -> Result<ResvgBuilder, NapiError> {
let js_options: JsOptions = options
.and_then(|o| serde_json::from_str(o.as_str()).ok())
.unwrap_or_default();
let _ = env_logger::builder()
.filter_level(js_options.log_level)
.try_init();
let mut opts = js_options.to_usvg_options();
crate::options::tweak_usvg_options(&mut opts);
let data = match svg {
Either::A(a) => a.as_str(),
Either::B(b) => std::str::from_utf8(b.as_ref())
.map_err(|e| napi::Error::from_reason(format!("{}", e)))?,
};
ResvgBuilderTryBuilder {
js_options,
data: data.to_string(),
doc_builder: |input| Document::parse(input),
}
.try_build()
.map_err(|e| napi::Error::from_reason(format!("{}", e)))
}

#[napi]
pub fn texts_to_resolve(&self) -> Vec<FontKeyWrapper> {
self.borrow_doc()
.descendants()
.filter_map(|node| {
let name = node.tag_name().name();
if name == "text" || name == "tspan" {
let family = resolve_font_family(&node).unwrap_or_else(|| {
self.borrow_js_options().font.default_font_family.as_str()
});
let width = node
.attribute("font-stretch")
.and_then(|s| s.parse::<Width>().ok())
.unwrap_or(Width::from(5));
let weight = resolve_font_weight(&node);
let italic = node
.attribute("font-style")
.map(|s| s == "italic")
.unwrap_or_default();
let font_key = FontKey::new(family, weight.0 as u32, italic, width);
Some(FontKeyWrapper(font_key))
} else {
None
}
})
.collect()
}

pub fn resolve_font(&mut self, font: Buffer) {
self.with_js_options_mut(|opts| opts.font_db.load_font_data(font.into()));
}

pub fn build(self) -> Result<Resvg, NapiError> {
let ouroboros_impl_resvg_builder::Heads { js_options, data } = self.into_heads();
let mut opts = js_options.to_usvg_options();
crate::options::tweak_usvg_options(&mut opts);
let opts_ref = opts.to_ref();
let tree = usvg::Tree::from_str(data.as_str(), &opts_ref)
.map_err(|e| napi::Error::from_reason(format!("{}", e)))?;
Ok(Resvg { tree, js_options })
}

// fn new_inner(
// svg: &Either<String, Buffer>,
// options: Option<String>,
// ) -> Result<Resvg, NapiError> {
// let opts_ref = opts.to_ref();
// // Parse the SVG string into a tree.
// let tree = match svg {
// Either::A(a) => usvg::Tree::from_str(a.as_str(), &opts_ref),
// Either::B(b) => usvg::Tree::from_data(b.as_ref(), &opts_ref),
// }
// .map_err(|e| napi::Error::from_reason(format!("{}", e)))?;
// Ok(Resvg { tree, js_options })
// }
}

fn resolve_font_family<'a, 'input>(node: &roxmltree::Node<'a, 'input>) -> Option<&'a str> {
for n in node.ancestors() {
if let Some(family) = n.attribute("font-family") {
return Some(family);
}
}
None
}

// This method is extracted from usvg to keep the logic here is the same with usvg
fn resolve_font_weight<'a, 'input>(node: &roxmltree::Node<'a, 'input>) -> fontdb::Weight {
fn bound(min: usize, val: usize, max: usize) -> usize {
std::cmp::max(min, std::cmp::min(max, val))
}

let nodes: Vec<_> = node.ancestors().collect();
let mut weight = 400;
for n in nodes.iter().rev().skip(1) {
// skip Root
weight = match n.attribute("font-weight").unwrap_or("") {
"normal" => 400,
"bold" => 700,
"100" => 100,
"200" => 200,
"300" => 300,
"400" => 400,
"500" => 500,
"600" => 600,
"700" => 700,
"800" => 800,
"900" => 900,
"bolder" => {
// By the CSS2 spec the default value should be 400
// so `bolder` will result in 500.
// But Chrome and Inkscape will give us 700.
// Have no idea is it a bug or something, but
// we will follow such behavior for now.
let step = if weight == 400 { 300 } else { 100 };

bound(100, weight + step, 900)
}
"lighter" => {
// By the CSS2 spec the default value should be 400
// so `lighter` will result in 300.
// But Chrome and Inkscape will give us 200.
// Have no idea is it a bug or something, but
// we will follow such behavior for now.
let step = if weight == 400 { 200 } else { 100 };

bound(100, weight - step, 900)
}
_ => weight,
};
}

fontdb::Weight(weight as u16)
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl ResvgBuilder {
#[wasm_bindgen(constructor)]
pub fn new(svg: IStringOrBuffer, options: Option<String>) -> Result<Resvg, js_sys::Error> {
let js_options: JsOptions = options
.and_then(|o| serde_json::from_str(o.as_str()).ok())
.unwrap_or_default();

let mut opts = js_options.to_usvg_options();
options::tweak_usvg_options(&mut opts);
let opts_ref = opts.to_ref();
let tree = if js_sys::Uint8Array::instanceof(&svg) {
let uintarray = js_sys::Uint8Array::unchecked_from_js_ref(&svg);
let svg_buffer = uintarray.to_vec();
usvg::Tree::from_data(&svg_buffer, &opts_ref).map_err(Error::from)
} else if let Some(s) = svg.as_string() {
usvg::Tree::from_str(s.as_str(), &opts_ref).map_err(Error::from)
} else {
Err(Error::InvalidInput)
}?;
Ok(Resvg { tree, js_options })
}
}
6 changes: 1 addition & 5 deletions src/fonts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ use usvg::fontdb::Database;

/// Loads fonts.
#[cfg(not(target_arch = "wasm32"))]
pub fn load_fonts(font_options: &JsFontOptions) -> Database {
// Create a new font database
let mut fontdb = Database::new();
pub fn load_fonts(font_options: &JsFontOptions, fontdb: &mut Database) {
let now = std::time::Instant::now();

// 加载系统字体
Expand Down Expand Up @@ -78,6 +76,4 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database {
warn!("Warning: The default font '{}' not found.", font_family);
}
}

fontdb
}
53 changes: 3 additions & 50 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use pathfinder_content::{
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::Vector2F;

use builder::ResvgBuilder;
#[cfg(not(target_arch = "wasm32"))]
use napi_derive::napi;
use options::JsOptions;
Expand All @@ -26,6 +27,7 @@ use wasm_bindgen::{
JsCast,
};

mod builder;
mod error;
mod fonts;
mod options;
Expand Down Expand Up @@ -125,34 +127,6 @@ impl RenderedImage {
#[cfg(not(target_arch = "wasm32"))]
#[napi]
impl Resvg {
#[napi(constructor)]
pub fn new(svg: Either<String, Buffer>, options: Option<String>) -> Result<Resvg, NapiError> {
Resvg::new_inner(&svg, options)
}

fn new_inner(
svg: &Either<String, Buffer>,
options: Option<String>,
) -> Result<Resvg, NapiError> {
let js_options: JsOptions = options
.and_then(|o| serde_json::from_str(o.as_str()).ok())
.unwrap_or_default();
let _ = env_logger::builder()
.filter_level(js_options.log_level)
.try_init();

let mut opts = js_options.to_usvg_options();
options::tweak_usvg_options(&mut opts);
let opts_ref = opts.to_ref();
// Parse the SVG string into a tree.
let tree = match svg {
Either::A(a) => usvg::Tree::from_str(a.as_str(), &opts_ref),
Either::B(b) => usvg::Tree::from_data(b.as_ref(), &opts_ref),
}
.map_err(|e| napi::Error::from_reason(format!("{}", e)))?;
Ok(Resvg { tree, js_options })
}

#[napi]
/// Renders an SVG in Node.js
pub fn render(&self) -> Result<RenderedImage, NapiError> {
Expand Down Expand Up @@ -265,27 +239,6 @@ impl Resvg {
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
impl Resvg {
#[wasm_bindgen(constructor)]
pub fn new(svg: IStringOrBuffer, options: Option<String>) -> Result<Resvg, js_sys::Error> {
let js_options: JsOptions = options
.and_then(|o| serde_json::from_str(o.as_str()).ok())
.unwrap_or_default();

let mut opts = js_options.to_usvg_options();
options::tweak_usvg_options(&mut opts);
let opts_ref = opts.to_ref();
let tree = if js_sys::Uint8Array::instanceof(&svg) {
let uintarray = js_sys::Uint8Array::unchecked_from_js_ref(&svg);
let svg_buffer = uintarray.to_vec();
usvg::Tree::from_data(&svg_buffer, &opts_ref).map_err(Error::from)
} else if let Some(s) = svg.as_string() {
usvg::Tree::from_str(s.as_str(), &opts_ref).map_err(Error::from)
} else {
Err(Error::InvalidInput)
}?;
Ok(Resvg { tree, js_options })
}

/// Get the SVG width
#[wasm_bindgen(getter)]
pub fn width(&self) -> f64 {
Expand Down Expand Up @@ -633,7 +586,7 @@ impl Task for AsyncRenderer {
type JsValue = RenderedImage;

fn compute(&mut self) -> Result<Self::Output, NapiError> {
let resvg = Resvg::new_inner(&self.svg, self.options.clone())?;
let resvg = ResvgBuilder::new_napi_inner(&self.svg, self.options.clone())?.build()?;
Ok(resvg.render()?)
}

Expand Down
12 changes: 6 additions & 6 deletions src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ pub struct JsOptions {

#[serde(with = "LogLevelDef")]
pub log_level: log::LevelFilter,

#[serde(skip)]
pub font_db: Database,
}

impl Default for JsOptions {
Expand All @@ -148,19 +151,17 @@ impl Default for JsOptions {
background: None,
crop: JsCropOptions::default(),
log_level: log::LevelFilter::Error,
font_db: Database::new(),
}
}
}

impl JsOptions {
pub(crate) fn to_usvg_options(&self) -> usvg::Options {
// Load fonts
let mut fontdb = self.font_db.clone();
#[cfg(not(target_arch = "wasm32"))]
let fontdb = if cfg!(target_arch = "wasm32") {
Database::new()
} else {
crate::fonts::load_fonts(&self.font)
};
crate::fonts::load_fonts(&self.font, &mut fontdb);

// Build the SVG options
usvg::Options {
Expand All @@ -174,7 +175,6 @@ impl JsOptions {
image_rendering: self.image_rendering,
keep_named_groups: false,
default_size: usvg::Size::new(100.0_f64, 100.0_f64).unwrap(),
#[cfg(not(target_arch = "wasm32"))]
fontdb,
image_href_resolver: usvg::ImageHrefResolver::default(),
}
Expand Down

0 comments on commit 5f96aa8

Please sign in to comment.