Skip to content

Commit 5f96aa8

Browse files
committed
feat: add font resolve support and builder api
1 parent 4639aa4 commit 5f96aa8

File tree

5 files changed

+215
-61
lines changed

5 files changed

+215
-61
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ pathfinder_content = { version = "0.5.0", default-features = false }
2828
pathfinder_simd = { version = "0.5.1", features = ["pf-no-simd"] }
2929
futures = "0.3.21"
3030
infer = "0.9.0"
31+
ouroboros = "0.15.0"
32+
roxmltree = "0.14.1"
33+
fontkit = "0.1.0"
3134

3235
[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]
3336
mimalloc-rust = { version = "0.2" }
@@ -38,6 +41,7 @@ js-sys = "0.3.58"
3841
usvg = { version = "0.22.0", default-features = false, features = [
3942
"export",
4043
"filter",
44+
"text"
4145
] }
4246

4347
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

src/builder.rs

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
use fontdb::{Database, Weight};
2+
use fontkit::{FontKey, Width};
3+
#[cfg(not(target_arch = "wasm32"))]
4+
use napi::bindgen_prelude::{Buffer, Either, Error as NapiError};
5+
#[cfg(not(target_arch = "wasm32"))]
6+
use napi_derive::napi;
7+
use ouroboros::self_referencing;
8+
use roxmltree::Document;
9+
10+
use crate::{options::JsOptions, Resvg};
11+
12+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
13+
#[cfg_attr(not(target_arch = "wasm32"), napi)]
14+
#[ouroboros::self_referencing]
15+
pub struct ResvgBuilder {
16+
js_options: JsOptions,
17+
data: String,
18+
#[borrows(data)]
19+
#[covariant]
20+
doc: Document<'this>,
21+
}
22+
23+
#[napi(js_name = "FontKey")]
24+
pub struct FontKeyWrapper(FontKey);
25+
26+
#[cfg(not(target_arch = "wasm32"))]
27+
#[napi]
28+
impl ResvgBuilder {
29+
#[napi(constructor)]
30+
pub fn new_napi(
31+
svg: Either<String, Buffer>,
32+
options: Option<String>,
33+
) -> Result<ResvgBuilder, NapiError> {
34+
ResvgBuilder::new_napi_inner(&svg, options)
35+
}
36+
37+
pub fn new_napi_inner(
38+
svg: &Either<String, Buffer>,
39+
options: Option<String>,
40+
) -> Result<ResvgBuilder, NapiError> {
41+
let js_options: JsOptions = options
42+
.and_then(|o| serde_json::from_str(o.as_str()).ok())
43+
.unwrap_or_default();
44+
let _ = env_logger::builder()
45+
.filter_level(js_options.log_level)
46+
.try_init();
47+
let mut opts = js_options.to_usvg_options();
48+
crate::options::tweak_usvg_options(&mut opts);
49+
let data = match svg {
50+
Either::A(a) => a.as_str(),
51+
Either::B(b) => std::str::from_utf8(b.as_ref())
52+
.map_err(|e| napi::Error::from_reason(format!("{}", e)))?,
53+
};
54+
ResvgBuilderTryBuilder {
55+
js_options,
56+
data: data.to_string(),
57+
doc_builder: |input| Document::parse(input),
58+
}
59+
.try_build()
60+
.map_err(|e| napi::Error::from_reason(format!("{}", e)))
61+
}
62+
63+
#[napi]
64+
pub fn texts_to_resolve(&self) -> Vec<FontKeyWrapper> {
65+
self.borrow_doc()
66+
.descendants()
67+
.filter_map(|node| {
68+
let name = node.tag_name().name();
69+
if name == "text" || name == "tspan" {
70+
let family = resolve_font_family(&node).unwrap_or_else(|| {
71+
self.borrow_js_options().font.default_font_family.as_str()
72+
});
73+
let width = node
74+
.attribute("font-stretch")
75+
.and_then(|s| s.parse::<Width>().ok())
76+
.unwrap_or(Width::from(5));
77+
let weight = resolve_font_weight(&node);
78+
let italic = node
79+
.attribute("font-style")
80+
.map(|s| s == "italic")
81+
.unwrap_or_default();
82+
let font_key = FontKey::new(family, weight.0 as u32, italic, width);
83+
Some(FontKeyWrapper(font_key))
84+
} else {
85+
None
86+
}
87+
})
88+
.collect()
89+
}
90+
91+
pub fn resolve_font(&mut self, font: Buffer) {
92+
self.with_js_options_mut(|opts| opts.font_db.load_font_data(font.into()));
93+
}
94+
95+
pub fn build(self) -> Result<Resvg, NapiError> {
96+
let ouroboros_impl_resvg_builder::Heads { js_options, data } = self.into_heads();
97+
let mut opts = js_options.to_usvg_options();
98+
crate::options::tweak_usvg_options(&mut opts);
99+
let opts_ref = opts.to_ref();
100+
let tree = usvg::Tree::from_str(data.as_str(), &opts_ref)
101+
.map_err(|e| napi::Error::from_reason(format!("{}", e)))?;
102+
Ok(Resvg { tree, js_options })
103+
}
104+
105+
// fn new_inner(
106+
// svg: &Either<String, Buffer>,
107+
// options: Option<String>,
108+
// ) -> Result<Resvg, NapiError> {
109+
// let opts_ref = opts.to_ref();
110+
// // Parse the SVG string into a tree.
111+
// let tree = match svg {
112+
// Either::A(a) => usvg::Tree::from_str(a.as_str(), &opts_ref),
113+
// Either::B(b) => usvg::Tree::from_data(b.as_ref(), &opts_ref),
114+
// }
115+
// .map_err(|e| napi::Error::from_reason(format!("{}", e)))?;
116+
// Ok(Resvg { tree, js_options })
117+
// }
118+
}
119+
120+
fn resolve_font_family<'a, 'input>(node: &roxmltree::Node<'a, 'input>) -> Option<&'a str> {
121+
for n in node.ancestors() {
122+
if let Some(family) = n.attribute("font-family") {
123+
return Some(family);
124+
}
125+
}
126+
None
127+
}
128+
129+
// This method is extracted from usvg to keep the logic here is the same with usvg
130+
fn resolve_font_weight<'a, 'input>(node: &roxmltree::Node<'a, 'input>) -> fontdb::Weight {
131+
fn bound(min: usize, val: usize, max: usize) -> usize {
132+
std::cmp::max(min, std::cmp::min(max, val))
133+
}
134+
135+
let nodes: Vec<_> = node.ancestors().collect();
136+
let mut weight = 400;
137+
for n in nodes.iter().rev().skip(1) {
138+
// skip Root
139+
weight = match n.attribute("font-weight").unwrap_or("") {
140+
"normal" => 400,
141+
"bold" => 700,
142+
"100" => 100,
143+
"200" => 200,
144+
"300" => 300,
145+
"400" => 400,
146+
"500" => 500,
147+
"600" => 600,
148+
"700" => 700,
149+
"800" => 800,
150+
"900" => 900,
151+
"bolder" => {
152+
// By the CSS2 spec the default value should be 400
153+
// so `bolder` will result in 500.
154+
// But Chrome and Inkscape will give us 700.
155+
// Have no idea is it a bug or something, but
156+
// we will follow such behavior for now.
157+
let step = if weight == 400 { 300 } else { 100 };
158+
159+
bound(100, weight + step, 900)
160+
}
161+
"lighter" => {
162+
// By the CSS2 spec the default value should be 400
163+
// so `lighter` will result in 300.
164+
// But Chrome and Inkscape will give us 200.
165+
// Have no idea is it a bug or something, but
166+
// we will follow such behavior for now.
167+
let step = if weight == 400 { 200 } else { 100 };
168+
169+
bound(100, weight - step, 900)
170+
}
171+
_ => weight,
172+
};
173+
}
174+
175+
fontdb::Weight(weight as u16)
176+
}
177+
178+
#[cfg(target_arch = "wasm32")]
179+
#[wasm_bindgen]
180+
impl ResvgBuilder {
181+
#[wasm_bindgen(constructor)]
182+
pub fn new(svg: IStringOrBuffer, options: Option<String>) -> Result<Resvg, js_sys::Error> {
183+
let js_options: JsOptions = options
184+
.and_then(|o| serde_json::from_str(o.as_str()).ok())
185+
.unwrap_or_default();
186+
187+
let mut opts = js_options.to_usvg_options();
188+
options::tweak_usvg_options(&mut opts);
189+
let opts_ref = opts.to_ref();
190+
let tree = if js_sys::Uint8Array::instanceof(&svg) {
191+
let uintarray = js_sys::Uint8Array::unchecked_from_js_ref(&svg);
192+
let svg_buffer = uintarray.to_vec();
193+
usvg::Tree::from_data(&svg_buffer, &opts_ref).map_err(Error::from)
194+
} else if let Some(s) = svg.as_string() {
195+
usvg::Tree::from_str(s.as_str(), &opts_ref).map_err(Error::from)
196+
} else {
197+
Err(Error::InvalidInput)
198+
}?;
199+
Ok(Resvg { tree, js_options })
200+
}
201+
}

src/fonts.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ use usvg::fontdb::Database;
1313

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

2119
// 加载系统字体
@@ -78,6 +76,4 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database {
7876
warn!("Warning: The default font '{}' not found.", font_family);
7977
}
8078
}
81-
82-
fontdb
8379
}

src/lib.rs

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use pathfinder_content::{
1515
use pathfinder_geometry::rect::RectF;
1616
use pathfinder_geometry::vector::Vector2F;
1717

18+
use builder::ResvgBuilder;
1819
#[cfg(not(target_arch = "wasm32"))]
1920
use napi_derive::napi;
2021
use options::JsOptions;
@@ -26,6 +27,7 @@ use wasm_bindgen::{
2627
JsCast,
2728
};
2829

30+
mod builder;
2931
mod error;
3032
mod fonts;
3133
mod options;
@@ -125,34 +127,6 @@ impl RenderedImage {
125127
#[cfg(not(target_arch = "wasm32"))]
126128
#[napi]
127129
impl Resvg {
128-
#[napi(constructor)]
129-
pub fn new(svg: Either<String, Buffer>, options: Option<String>) -> Result<Resvg, NapiError> {
130-
Resvg::new_inner(&svg, options)
131-
}
132-
133-
fn new_inner(
134-
svg: &Either<String, Buffer>,
135-
options: Option<String>,
136-
) -> Result<Resvg, NapiError> {
137-
let js_options: JsOptions = options
138-
.and_then(|o| serde_json::from_str(o.as_str()).ok())
139-
.unwrap_or_default();
140-
let _ = env_logger::builder()
141-
.filter_level(js_options.log_level)
142-
.try_init();
143-
144-
let mut opts = js_options.to_usvg_options();
145-
options::tweak_usvg_options(&mut opts);
146-
let opts_ref = opts.to_ref();
147-
// Parse the SVG string into a tree.
148-
let tree = match svg {
149-
Either::A(a) => usvg::Tree::from_str(a.as_str(), &opts_ref),
150-
Either::B(b) => usvg::Tree::from_data(b.as_ref(), &opts_ref),
151-
}
152-
.map_err(|e| napi::Error::from_reason(format!("{}", e)))?;
153-
Ok(Resvg { tree, js_options })
154-
}
155-
156130
#[napi]
157131
/// Renders an SVG in Node.js
158132
pub fn render(&self) -> Result<RenderedImage, NapiError> {
@@ -265,27 +239,6 @@ impl Resvg {
265239
#[cfg(target_arch = "wasm32")]
266240
#[wasm_bindgen]
267241
impl Resvg {
268-
#[wasm_bindgen(constructor)]
269-
pub fn new(svg: IStringOrBuffer, options: Option<String>) -> Result<Resvg, js_sys::Error> {
270-
let js_options: JsOptions = options
271-
.and_then(|o| serde_json::from_str(o.as_str()).ok())
272-
.unwrap_or_default();
273-
274-
let mut opts = js_options.to_usvg_options();
275-
options::tweak_usvg_options(&mut opts);
276-
let opts_ref = opts.to_ref();
277-
let tree = if js_sys::Uint8Array::instanceof(&svg) {
278-
let uintarray = js_sys::Uint8Array::unchecked_from_js_ref(&svg);
279-
let svg_buffer = uintarray.to_vec();
280-
usvg::Tree::from_data(&svg_buffer, &opts_ref).map_err(Error::from)
281-
} else if let Some(s) = svg.as_string() {
282-
usvg::Tree::from_str(s.as_str(), &opts_ref).map_err(Error::from)
283-
} else {
284-
Err(Error::InvalidInput)
285-
}?;
286-
Ok(Resvg { tree, js_options })
287-
}
288-
289242
/// Get the SVG width
290243
#[wasm_bindgen(getter)]
291244
pub fn width(&self) -> f64 {
@@ -633,7 +586,7 @@ impl Task for AsyncRenderer {
633586
type JsValue = RenderedImage;
634587

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

src/options.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ pub struct JsOptions {
133133

134134
#[serde(with = "LogLevelDef")]
135135
pub log_level: log::LevelFilter,
136+
137+
#[serde(skip)]
138+
pub font_db: Database,
136139
}
137140

138141
impl Default for JsOptions {
@@ -148,19 +151,17 @@ impl Default for JsOptions {
148151
background: None,
149152
crop: JsCropOptions::default(),
150153
log_level: log::LevelFilter::Error,
154+
font_db: Database::new(),
151155
}
152156
}
153157
}
154158

155159
impl JsOptions {
156160
pub(crate) fn to_usvg_options(&self) -> usvg::Options {
157161
// Load fonts
162+
let mut fontdb = self.font_db.clone();
158163
#[cfg(not(target_arch = "wasm32"))]
159-
let fontdb = if cfg!(target_arch = "wasm32") {
160-
Database::new()
161-
} else {
162-
crate::fonts::load_fonts(&self.font)
163-
};
164+
crate::fonts::load_fonts(&self.font, &mut fontdb);
164165

165166
// Build the SVG options
166167
usvg::Options {
@@ -174,7 +175,6 @@ impl JsOptions {
174175
image_rendering: self.image_rendering,
175176
keep_named_groups: false,
176177
default_size: usvg::Size::new(100.0_f64, 100.0_f64).unwrap(),
177-
#[cfg(not(target_arch = "wasm32"))]
178178
fontdb,
179179
image_href_resolver: usvg::ImageHrefResolver::default(),
180180
}

0 commit comments

Comments
 (0)