Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add font resolve support and builder api #118

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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