From 0b33ace4cb9cbe272bfa051eaf60a1458975420b Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 1 Jun 2024 13:28:50 +0200 Subject: [PATCH] Support user-provided font loading callbacks (#769) --- Cargo.lock | 4 +- crates/c-api/lib.rs | 75 ++----- crates/resvg/examples/custom_href_resolver.rs | 18 +- crates/resvg/examples/draw_bboxes.rs | 7 +- crates/resvg/examples/minimal.rs | 7 +- crates/resvg/src/main.rs | 12 +- crates/resvg/tests/integration/main.rs | 21 +- crates/usvg/Cargo.toml | 2 +- crates/usvg/src/main.rs | 5 +- crates/usvg/src/parser/converter.rs | 71 ++++--- crates/usvg/src/parser/image.rs | 160 ++++++-------- crates/usvg/src/parser/mod.rs | 46 +--- crates/usvg/src/parser/options.rs | 42 +++- crates/usvg/src/parser/text.rs | 2 +- crates/usvg/src/text/flatten.rs | 9 +- crates/usvg/src/text/layout.rs | 134 ++---------- crates/usvg/src/text/mod.rs | 196 +++++++++++++++++- crates/usvg/src/tree/mod.rs | 8 + crates/usvg/tests/parser.rs | 39 ++-- crates/usvg/tests/write.rs | 14 +- tools/explorer-thumbnailer/src/utils.rs | 8 +- 21 files changed, 455 insertions(+), 425 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7625ddeee..490c67c71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "fontdb" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2894eb653f564384ae8da32b78d9c8db0394715525d917328e435b9babd902" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" dependencies = [ "fontconfig-parser", "log", diff --git a/crates/c-api/lib.rs b/crates/c-api/lib.rs index 36933814d..9ec66d746 100644 --- a/crates/c-api/lib.rs +++ b/crates/c-api/lib.rs @@ -14,8 +14,6 @@ use std::slice; use resvg::tiny_skia; use resvg::usvg; -#[cfg(feature = "text")] -use resvg::usvg::fontdb; /// @brief List of possible errors. #[repr(C)] @@ -113,9 +111,7 @@ pub extern "C" fn resvg_init_log() { /// Also, contains a fonts database used during text to path conversion. /// The database is empty by default. pub struct resvg_options { - options: usvg::Options, - #[cfg(feature = "text")] - fontdb: fontdb::Database, + options: usvg::Options<'static>, } /// @brief Creates a new #resvg_options object. @@ -125,28 +121,17 @@ pub struct resvg_options { pub extern "C" fn resvg_options_create() -> *mut resvg_options { Box::into_raw(Box::new(resvg_options { options: usvg::Options::default(), - #[cfg(feature = "text")] - fontdb: fontdb::Database::new(), })) } #[inline] -fn cast_opt(opt: *mut resvg_options) -> &'static mut usvg::Options { +fn cast_opt(opt: *mut resvg_options) -> &'static mut usvg::Options<'static> { unsafe { assert!(!opt.is_null()); &mut (*opt).options } } -#[cfg(feature = "text")] -#[inline] -fn cast_fontdb(opt: *mut resvg_options) -> &'static mut fontdb::Database { - unsafe { - assert!(!opt.is_null()); - &mut (*opt).fontdb - } -} - /// @brief Sets a directory that will be used during relative paths resolving. /// /// Expected to be the same as the directory that contains the SVG file, @@ -208,7 +193,9 @@ pub extern "C" fn resvg_options_set_font_size(opt: *mut resvg_options, size: f32 pub extern "C" fn resvg_options_set_serif_family(opt: *mut resvg_options, family: *const c_char) { #[cfg(feature = "text")] { - cast_fontdb(opt).set_serif_family(cstr_to_str(family).unwrap().to_string()); + cast_opt(opt) + .fontdb_mut() + .set_serif_family(cstr_to_str(family).unwrap().to_string()); } } @@ -227,7 +214,9 @@ pub extern "C" fn resvg_options_set_sans_serif_family( ) { #[cfg(feature = "text")] { - cast_fontdb(opt).set_sans_serif_family(cstr_to_str(family).unwrap().to_string()); + cast_opt(opt) + .fontdb_mut() + .set_sans_serif_family(cstr_to_str(family).unwrap().to_string()); } } @@ -243,7 +232,9 @@ pub extern "C" fn resvg_options_set_sans_serif_family( pub extern "C" fn resvg_options_set_cursive_family(opt: *mut resvg_options, family: *const c_char) { #[cfg(feature = "text")] { - cast_fontdb(opt).set_cursive_family(cstr_to_str(family).unwrap().to_string()); + cast_opt(opt) + .fontdb_mut() + .set_cursive_family(cstr_to_str(family).unwrap().to_string()); } } @@ -259,7 +250,9 @@ pub extern "C" fn resvg_options_set_cursive_family(opt: *mut resvg_options, fami pub extern "C" fn resvg_options_set_fantasy_family(opt: *mut resvg_options, family: *const c_char) { #[cfg(feature = "text")] { - cast_fontdb(opt).set_fantasy_family(cstr_to_str(family).unwrap().to_string()); + cast_opt(opt) + .fontdb_mut() + .set_fantasy_family(cstr_to_str(family).unwrap().to_string()); } } @@ -278,7 +271,9 @@ pub extern "C" fn resvg_options_set_monospace_family( ) { #[cfg(feature = "text")] { - cast_fontdb(opt).set_monospace_family(cstr_to_str(family).unwrap().to_string()); + cast_opt(opt) + .fontdb_mut() + .set_monospace_family(cstr_to_str(family).unwrap().to_string()); } } @@ -408,13 +403,7 @@ pub extern "C" fn resvg_options_load_font_data( #[cfg(feature = "text")] { let data = unsafe { slice::from_raw_parts(data as *const u8, len) }; - - let opt = unsafe { - assert!(!opt.is_null()); - &mut *opt - }; - - opt.fontdb.load_font_data(data.to_vec()) + cast_opt(opt).fontdb_mut().load_font_data(data.to_vec()) } } @@ -438,12 +427,7 @@ pub extern "C" fn resvg_options_load_font_file( None => return resvg_error::NOT_AN_UTF8_STR as i32, }; - let opt = unsafe { - assert!(!opt.is_null()); - &mut *opt - }; - - if opt.fontdb.load_font_file(file_path).is_ok() { + if cast_opt(opt).fontdb_mut().load_font_file(file_path).is_ok() { resvg_error::OK as i32 } else { resvg_error::FILE_OPEN_FAILED as i32 @@ -473,12 +457,7 @@ pub extern "C" fn resvg_options_load_font_file( pub extern "C" fn resvg_options_load_system_fonts(opt: *mut resvg_options) { #[cfg(feature = "text")] { - let opt = unsafe { - assert!(!opt.is_null()); - &mut *opt - }; - - opt.fontdb.load_system_fonts(); + cast_opt(opt).fontdb_mut().load_system_fonts(); } } @@ -526,12 +505,7 @@ pub extern "C" fn resvg_parse_tree_from_file( Err(_) => return resvg_error::FILE_OPEN_FAILED as i32, }; - let utree = usvg::Tree::from_data( - &file_data, - &raw_opt.options, - #[cfg(feature = "text")] - &raw_opt.fontdb, - ); + let utree = usvg::Tree::from_data(&file_data, &raw_opt.options); let utree = match utree { Ok(tree) => tree, @@ -569,12 +543,7 @@ pub extern "C" fn resvg_parse_tree_from_data( &*opt }; - let utree = usvg::Tree::from_data( - data, - &raw_opt.options, - #[cfg(feature = "text")] - &raw_opt.fontdb, - ); + let utree = usvg::Tree::from_data(data, &raw_opt.options); let utree = match utree { Ok(tree) => tree, diff --git a/crates/resvg/examples/custom_href_resolver.rs b/crates/resvg/examples/custom_href_resolver.rs index c7015ceb6..7c6e39605 100644 --- a/crates/resvg/examples/custom_href_resolver.rs +++ b/crates/resvg/examples/custom_href_resolver.rs @@ -4,18 +4,14 @@ fn main() { let ferris_image = std::sync::Arc::new(std::fs::read("./examples/ferris.png").unwrap()); // We know that our SVG won't have DataUrl hrefs, just return None for such case. - let resolve_data = Box::new( - |_: &str, _: std::sync::Arc>, _: &usvg::Options, _: &usvg::fontdb::Database| None, - ); + let resolve_data = Box::new(|_: &str, _: std::sync::Arc>, _: &usvg::Options| None); // Here we handle xlink:href attribute as string, // let's use already loaded Ferris image to match that string. - let resolve_string = Box::new( - move |href: &str, _: &usvg::Options, _: &usvg::fontdb::Database| match href { - "ferris_image" => Some(usvg::ImageKind::PNG(ferris_image.clone())), - _ => None, - }, - ); + let resolve_string = Box::new(move |href: &str, _: &usvg::Options| match href { + "ferris_image" => Some(usvg::ImageKind::PNG(ferris_image.clone())), + _ => None, + }); // Assign new ImageHrefResolver option using our closures. opt.image_href_resolver = usvg::ImageHrefResolver { @@ -23,10 +19,8 @@ fn main() { resolve_string, }; - let fontdb = usvg::fontdb::Database::new(); - let svg_data = std::fs::read("./examples/custom_href_resolver.svg").unwrap(); - let tree = usvg::Tree::from_data(&svg_data, &opt, &fontdb).unwrap(); + let tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); let pixmap_size = tree.size().to_int_size(); let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap(); diff --git a/crates/resvg/examples/draw_bboxes.rs b/crates/resvg/examples/draw_bboxes.rs index aa726feae..2fa545e76 100644 --- a/crates/resvg/examples/draw_bboxes.rs +++ b/crates/resvg/examples/draw_bboxes.rs @@ -1,5 +1,3 @@ -use usvg::fontdb; - fn main() { let args: Vec = std::env::args().collect(); if !(args.len() == 3 || args.len() == 5) { @@ -23,11 +21,10 @@ fn main() { .ok() .and_then(|p| p.parent().map(|p| p.to_path_buf())); - let mut fontdb = fontdb::Database::new(); - fontdb.load_system_fonts(); + opt.fontdb_mut().load_system_fonts(); let svg_data = std::fs::read(&args[1]).unwrap(); - let tree = usvg::Tree::from_data(&svg_data, &opt, &fontdb).unwrap(); + let tree = usvg::Tree::from_data(&svg_data, &opt).unwrap(); let mut bboxes = Vec::new(); let mut stroke_bboxes = Vec::new(); diff --git a/crates/resvg/examples/minimal.rs b/crates/resvg/examples/minimal.rs index a3d64d944..9b1b04461 100644 --- a/crates/resvg/examples/minimal.rs +++ b/crates/resvg/examples/minimal.rs @@ -1,5 +1,3 @@ -use usvg::fontdb; - fn main() { let args: Vec = std::env::args().collect(); if args.len() != 3 { @@ -14,11 +12,10 @@ fn main() { .ok() .and_then(|p| p.parent().map(|p| p.to_path_buf())); - let mut fontdb = fontdb::Database::new(); - fontdb.load_system_fonts(); + opt.fontdb_mut().load_system_fonts(); let svg_data = std::fs::read(&args[1]).unwrap(); - usvg::Tree::from_data(&svg_data, &opt, &fontdb).unwrap() + usvg::Tree::from_data(&svg_data, &opt).unwrap() }; let pixmap_size = tree.size().to_int_size(); diff --git a/crates/resvg/src/main.rs b/crates/resvg/src/main.rs index 1337ae364..be7277353 100644 --- a/crates/resvg/src/main.rs +++ b/crates/resvg/src/main.rs @@ -5,6 +5,7 @@ #![allow(clippy::uninlined_format_args)] use std::path; +use std::sync::Arc; use usvg::fontdb; @@ -30,7 +31,7 @@ where } fn process() -> Result<(), String> { - let args = match parse_args() { + let mut args = match parse_args() { Ok(args) => args, Err(e) => { println!("{}", HELP); @@ -85,15 +86,14 @@ fn process() -> Result<(), String> { .descendants() .any(|n| n.has_tag_name(("http://www.w3.org/2000/svg", "text"))); - let mut fontdb = fontdb::Database::new(); if has_text_nodes { timed(args.perf, "FontDB", || { - load_fonts(&args.raw_args, &mut fontdb) + load_fonts(&args.raw_args, args.usvg.fontdb_mut()) }); } let tree = timed(args.perf, "SVG Parsing", || { - usvg::Tree::from_xmltree(&xml_tree, &args.usvg, &fontdb).map_err(|e| e.to_string()) + usvg::Tree::from_xmltree(&xml_tree, &args.usvg).map_err(|e| e.to_string()) })?; if args.query_all { @@ -460,7 +460,7 @@ struct Args { export_area_drawing: bool, perf: bool, quiet: bool, - usvg: usvg::Options, + usvg: usvg::Options<'static>, fit_to: FitTo, background: Option, raw_args: CliArgs, // TODO: find a better way @@ -562,6 +562,8 @@ fn parse_args() -> Result { image_rendering: args.image_rendering, default_size, image_href_resolver: usvg::ImageHrefResolver::default(), + font_resolver: usvg::FontResolver::default(), + fontdb: Arc::new(fontdb::Database::new()), }; Ok(Args { diff --git a/crates/resvg/tests/integration/main.rs b/crates/resvg/tests/integration/main.rs index d7f363da7..11b1bfede 100644 --- a/crates/resvg/tests/integration/main.rs +++ b/crates/resvg/tests/integration/main.rs @@ -1,6 +1,7 @@ use once_cell::sync::Lazy; use rgb::{FromSlice, RGBA8}; use std::process::Command; +use std::sync::Arc; use usvg::fontdb; #[rustfmt::skip] @@ -10,7 +11,7 @@ mod extra; const IMAGE_SIZE: u32 = 300; -static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { +static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { if let Ok(()) = log::set_logger(&LOGGER) { log::set_max_level(log::LevelFilter::Warn); } @@ -22,7 +23,7 @@ static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { fontdb.set_cursive_family("Yellowtail"); fontdb.set_fantasy_family("Sedgwick Ave Display"); fontdb.set_monospace_family("Noto Mono"); - std::sync::Mutex::new(fontdb) + Arc::new(fontdb) }); pub fn render(name: &str) -> usize { @@ -36,11 +37,11 @@ pub fn render(name: &str) -> usize { .unwrap() .to_owned(), ); + opt.fontdb = GLOBAL_FONTDB.clone(); let tree = { let svg_data = std::fs::read(&svg_path).unwrap(); - let db = GLOBAL_FONTDB.lock().unwrap(); - usvg::Tree::from_data(&svg_data, &opt, &db).unwrap() + usvg::Tree::from_data(&svg_data, &opt).unwrap() }; let size = tree @@ -90,12 +91,12 @@ pub fn render_extra_with_scale(name: &str, scale: f32) -> usize { let svg_path = format!("tests/{}.svg", name); let png_path = format!("tests/{}.png", name); - let opt = usvg::Options::default(); + let mut opt = usvg::Options::default(); + opt.fontdb = GLOBAL_FONTDB.clone(); let tree = { let svg_data = std::fs::read(&svg_path).unwrap(); - let db = GLOBAL_FONTDB.lock().unwrap(); - usvg::Tree::from_data(&svg_data, &opt, &db).unwrap() + usvg::Tree::from_data(&svg_data, &opt).unwrap() }; let size = tree.size().to_int_size().scale_by(scale).unwrap(); @@ -140,12 +141,12 @@ pub fn render_node(name: &str, id: &str) -> usize { let svg_path = format!("tests/{}.svg", name); let png_path = format!("tests/{}.png", name); - let opt = usvg::Options::default(); + let mut opt = usvg::Options::default(); + opt.fontdb = GLOBAL_FONTDB.clone(); let tree = { let svg_data = std::fs::read(&svg_path).unwrap(); - let db = GLOBAL_FONTDB.lock().unwrap(); - usvg::Tree::from_data(&svg_data, &opt, &db).unwrap() + usvg::Tree::from_data(&svg_data, &opt).unwrap() }; let node = tree.node_by_id(id).unwrap(); diff --git a/crates/usvg/Cargo.toml b/crates/usvg/Cargo.toml index 9b5859a78..e2f66d4d3 100644 --- a/crates/usvg/Cargo.toml +++ b/crates/usvg/Cargo.toml @@ -36,7 +36,7 @@ simplecss = "0.2" siphasher = "1.0" # perfect hash implementation # text -fontdb = { version = "0.17.0", default-features = false, optional = true } +fontdb = { version = "0.18.0", default-features = false, optional = true } rustybuzz = { version = "0.14.0", optional = true } unicode-bidi = { version = "0.3", optional = true } unicode-script = { version = "0.5", optional = true } diff --git a/crates/usvg/src/main.rs b/crates/usvg/src/main.rs index 29c20d709..b85ba1526 100644 --- a/crates/usvg/src/main.rs +++ b/crates/usvg/src/main.rs @@ -6,6 +6,7 @@ use std::fs::File; use std::io::{self, Read, Write}; use std::path::PathBuf; use std::process; +use std::sync::Arc; use pico_args::Arguments; @@ -415,6 +416,8 @@ fn process(args: Args) -> Result<(), String> { default_size: usvg::Size::from_wh(args.default_width as f32, args.default_height as f32) .unwrap(), image_href_resolver: usvg::ImageHrefResolver::default(), + font_resolver: usvg::FontResolver::default(), + fontdb: Arc::new(fontdb), }; let input_svg = match in_svg { @@ -422,7 +425,7 @@ fn process(args: Args) -> Result<(), String> { InputFrom::File(ref path) => std::fs::read(path).map_err(|e| e.to_string()), }?; - let tree = usvg::Tree::from_data(&input_svg, &re_opt, &fontdb).map_err(|e| format!("{}", e))?; + let tree = usvg::Tree::from_data(&input_svg, &re_opt).map_err(|e| format!("{}", e))?; let xml_opt = usvg::WriteOptions { id_prefix: args.id_prefix, diff --git a/crates/usvg/src/parser/converter.rs b/crates/usvg/src/parser/converter.rs index a95a59124..fd11d06fd 100644 --- a/crates/usvg/src/parser/converter.rs +++ b/crates/usvg/src/parser/converter.rs @@ -7,6 +7,8 @@ use std::hash::{Hash, Hasher}; use std::str::FromStr; use std::sync::Arc; +#[cfg(feature = "text")] +use fontdb::Database; use svgtypes::{Length, LengthUnit as Unit, PaintOrderKind, TransformOrigin}; use super::svgtree::{self, AId, EId, FromValue, SvgNode}; @@ -29,13 +31,16 @@ pub struct State<'a> { /// Used only during nested `svg` size resolving. /// Width and height can be set independently. pub(crate) use_size: (Option, Option), - pub(crate) opt: &'a Options, - #[cfg(feature = "text")] - pub(crate) fontdb: &'a fontdb::Database, + pub(crate) opt: &'a Options<'a>, } -#[derive(Clone, Default)] +#[derive(Clone)] pub struct Cache { + /// This fontdb is initialized from [`Options::fontdb`] and then populated + /// over the course of conversion. + #[cfg(feature = "text")] + pub fontdb: Arc, + pub clip_paths: HashMap>, pub masks: HashMap>, pub filters: HashMap>, @@ -53,6 +58,27 @@ pub struct Cache { } impl Cache { + pub(crate) fn new(#[cfg(feature = "text")] fontdb: Arc) -> Self { + Self { + #[cfg(feature = "text")] + fontdb, + + clip_paths: HashMap::new(), + masks: HashMap::new(), + filters: HashMap::new(), + paint: HashMap::new(), + + all_ids: HashSet::new(), + linear_gradient_index: 0, + radial_gradient_index: 0, + pattern_index: 0, + clip_path_index: 0, + mask_index: 0, + filter_index: 0, + image_index: 0, + } + } + // TODO: macros? pub(crate) fn gen_linear_gradient_id(&mut self) -> NonEmptyString { loop { @@ -257,18 +283,9 @@ impl SvgColorExt for svgtypes::Color { /// /// - If `Document` doesn't have an SVG node - returns an empty tree. /// - If `Document` doesn't have a valid size - returns `Error::InvalidSize`. -pub(crate) fn convert_doc( - svg_doc: &svgtree::Document, - opt: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database, -) -> Result { +pub(crate) fn convert_doc(svg_doc: &svgtree::Document, opt: &Options) -> Result { let svg = svg_doc.root_element(); - let (size, restore_viewbox) = resolve_svg_size( - &svg, - opt, - #[cfg(feature = "text")] - fontdb, - ); + let (size, restore_viewbox) = resolve_svg_size(&svg, opt); let size = size?; let view_box = ViewBox { rect: svg @@ -286,6 +303,8 @@ pub(crate) fn convert_doc( clip_paths: Vec::new(), masks: Vec::new(), filters: Vec::new(), + #[cfg(feature = "text")] + fontdb: opt.fontdb.clone(), }; if !svg.is_visible_element(opt) { @@ -300,11 +319,12 @@ pub(crate) fn convert_doc( view_box: view_box.rect, use_size: (None, None), opt, - #[cfg(feature = "text")] - fontdb, }; - let mut cache = Cache::default(); + let mut cache = Cache::new( + #[cfg(feature = "text")] + opt.fontdb.clone(), + ); for node in svg_doc.descendants() { if let Some(tag) = node.tag_name() { @@ -355,6 +375,13 @@ pub(crate) fn convert_doc( tree.root.collect_filters(&mut tree.filters); tree.root.calculate_bounding_boxes(); + // The fontdb might have been mutated and we want to apply these changes to + // the tree's fontdb. + #[cfg(feature = "text")] + { + tree.fontdb = cache.fontdb; + } + if restore_viewbox { calculate_svg_bbox(&mut tree); } @@ -362,11 +389,7 @@ pub(crate) fn convert_doc( Ok(tree) } -fn resolve_svg_size( - svg: &SvgNode, - opt: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database, -) -> (Result, bool) { +fn resolve_svg_size(svg: &SvgNode, opt: &Options) -> (Result, bool) { let mut state = State { parent_clip_path: None, context_element: None, @@ -375,8 +398,6 @@ fn resolve_svg_size( view_box: NonZeroRect::from_xywh(0.0, 0.0, 100.0, 100.0).unwrap(), use_size: (None, None), opt, - #[cfg(feature = "text")] - fontdb, }; let def = Length::new(100.0, Unit::Percent); diff --git a/crates/usvg/src/parser/image.rs b/crates/usvg/src/parser/image.rs index 58a5ba165..8b1605d9a 100644 --- a/crates/usvg/src/parser/image.rs +++ b/crates/usvg/src/parser/image.rs @@ -14,41 +14,30 @@ use crate::{ }; /// A shorthand for [ImageHrefResolver]'s data function. -#[cfg(feature = "text")] -pub type ImageHrefDataResolverFn = - Box>, &Options, &fontdb::Database) -> Option + Send + Sync>; - -/// A shorthand for [ImageHrefResolver]'s data function. -#[cfg(not(feature = "text"))] -pub type ImageHrefDataResolverFn = - Box>, &Options) -> Option + Send + Sync>; - -/// A shorthand for [ImageHrefResolver]'s string function. -#[cfg(feature = "text")] -pub type ImageHrefStringResolverFn = - Box Option + Send + Sync>; +pub type ImageHrefDataResolverFn<'a> = + Box>, &Options) -> Option + Send + Sync + 'a>; /// A shorthand for [ImageHrefResolver]'s string function. -#[cfg(not(feature = "text"))] -pub type ImageHrefStringResolverFn = Box Option + Send + Sync>; +pub type ImageHrefStringResolverFn<'a> = + Box Option + Send + Sync + 'a>; /// An `xlink:href` resolver for `` elements. /// /// This type can be useful if you want to have an alternative `xlink:href` handling /// to the default one. For example, you can forbid access to local files (which is allowed by default) /// or add support for resolving actual URLs (usvg doesn't do any network requests). -pub struct ImageHrefResolver { +pub struct ImageHrefResolver<'a> { /// Resolver function that will be used when `xlink:href` contains a /// [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs). /// /// A function would be called with mime, decoded base64 data and parsing options. - pub resolve_data: ImageHrefDataResolverFn, + pub resolve_data: ImageHrefDataResolverFn<'a>, /// Resolver function that will be used to handle an arbitrary string in `xlink:href`. - pub resolve_string: ImageHrefStringResolverFn, + pub resolve_string: ImageHrefStringResolverFn<'a>, } -impl Default for ImageHrefResolver { +impl Default for ImageHrefResolver<'_> { fn default() -> Self { ImageHrefResolver { resolve_data: ImageHrefResolver::default_data_resolver(), @@ -57,7 +46,7 @@ impl Default for ImageHrefResolver { } } -impl ImageHrefResolver { +impl ImageHrefResolver<'_> { /// Creates a default /// [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) /// resolver closure. @@ -67,31 +56,18 @@ impl ImageHrefResolver { /// The default implementation would try to load JPEG, PNG, GIF, SVG and SVGZ types. /// Note that it will simply match the `mime` or data's magic. /// The actual images would not be decoded. It's up to the renderer. - pub fn default_data_resolver() -> ImageHrefDataResolverFn { + pub fn default_data_resolver() -> ImageHrefDataResolverFn<'static> { Box::new( - move |mime: &str, - data: Arc>, - opts: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database| match mime { + move |mime: &str, data: Arc>, opts: &Options| match mime { "image/jpg" | "image/jpeg" => Some(ImageKind::JPEG(data)), "image/png" => Some(ImageKind::PNG(data)), "image/gif" => Some(ImageKind::GIF(data)), - "image/svg+xml" => load_sub_svg( - &data, - opts, - #[cfg(feature = "text")] - fontdb, - ), + "image/svg+xml" => load_sub_svg(&data, opts), "text/plain" => match get_image_data_format(&data) { Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(data)), Some(ImageFormat::PNG) => Some(ImageKind::PNG(data)), Some(ImageFormat::GIF) => Some(ImageKind::GIF(data)), - _ => load_sub_svg( - &data, - opts, - #[cfg(feature = "text")] - fontdb, - ), + _ => load_sub_svg(&data, opts), }, _ => None, }, @@ -105,47 +81,38 @@ impl ImageHrefResolver { /// /// Paths have to be absolute or relative to the input SVG file or relative to /// [Options::resources_dir](crate::Options::resources_dir). - pub fn default_string_resolver() -> ImageHrefStringResolverFn { - Box::new( - move |href: &str, - opts: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database| { - let path = opts.get_abs_path(std::path::Path::new(href)); - - if path.exists() { - let data = match std::fs::read(&path) { - Ok(data) => data, - Err(_) => { - log::warn!("Failed to load '{}'. Skipped.", href); - return None; - } - }; - - match get_image_file_format(&path, &data) { - Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))), - Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))), - Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))), - Some(ImageFormat::SVG) => load_sub_svg( - &data, - opts, - #[cfg(feature = "text")] - fontdb, - ), - _ => { - log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href); - None - } + pub fn default_string_resolver() -> ImageHrefStringResolverFn<'static> { + Box::new(move |href: &str, opts: &Options| { + let path = opts.get_abs_path(std::path::Path::new(href)); + + if path.exists() { + let data = match std::fs::read(&path) { + Ok(data) => data, + Err(_) => { + log::warn!("Failed to load '{}'. Skipped.", href); + return None; + } + }; + + match get_image_file_format(&path, &data) { + Some(ImageFormat::JPEG) => Some(ImageKind::JPEG(Arc::new(data))), + Some(ImageFormat::PNG) => Some(ImageKind::PNG(Arc::new(data))), + Some(ImageFormat::GIF) => Some(ImageKind::GIF(Arc::new(data))), + Some(ImageFormat::SVG) => load_sub_svg(&data, opts), + _ => { + log::warn!("'{}' is not a PNG, JPEG, GIF or SVG(Z) image.", href); + None } - } else { - log::warn!("'{}' is not a path to an image.", href); - None } - }, - ) + } else { + log::warn!("'{}' is not a path to an image.", href); + None + } + }) } } -impl std::fmt::Debug for ImageHrefResolver { +impl std::fmt::Debug for ImageHrefResolver<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("ImageHrefResolver { .. }") } @@ -323,20 +290,9 @@ pub(crate) fn get_href_data(href: &str, state: &converter::State) -> Option Option { /// /// Unlike `Tree::from_*` methods, this one will also remove all `image` elements /// from the loaded SVG, as required by the spec. -pub(crate) fn load_sub_svg( - data: &[u8], - opt: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database, -) -> Option { +pub(crate) fn load_sub_svg(data: &[u8], opt: &Options) -> Option { let mut sub_opt = Options::default(); sub_opt.resources_dir = None; sub_opt.dpi = opt.dpi; @@ -383,16 +335,26 @@ pub(crate) fn load_sub_svg( // The referenced SVG image cannot have any 'image' elements by itself. // Not only recursive. Any. Don't know why. sub_opt.image_href_resolver = ImageHrefResolver { - resolve_data: Box::new(|_, _, _, #[cfg(feature = "text")] _| None), - resolve_string: Box::new(|_, _, #[cfg(feature = "text")] _| None), + resolve_data: Box::new(|_, _, _| None), + resolve_string: Box::new(|_, _| None), }; - let tree = Tree::from_data( - data, - &sub_opt, - #[cfg(feature = "text")] - fontdb, - ); + #[cfg(feature = "text")] + { + // In the referenced SVG, we start with the unmodified user-provided + // fontdb, not the one from the cache. + sub_opt.fontdb = opt.fontdb.clone(); + + // Can't clone the resolver, so we create a new one that forwards to it. + sub_opt.font_resolver = crate::FontResolver { + select_font: Box::new(|font, db| (opt.font_resolver.select_font)(font, db)), + select_fallback: Box::new(|c, used_fonts, db| { + (opt.font_resolver.select_fallback)(c, used_fonts, db) + }), + }; + } + + let tree = Tree::from_data(data, &sub_opt); let tree = match tree { Ok(tree) => tree, Err(_) => { diff --git a/crates/usvg/src/parser/mod.rs b/crates/usvg/src/parser/mod.rs index 6382ee18c..1f52c3b85 100644 --- a/crates/usvg/src/parser/mod.rs +++ b/crates/usvg/src/parser/mod.rs @@ -95,37 +95,19 @@ impl crate::Tree { /// Parses `Tree` from an SVG data. /// /// Can contain an SVG string or a gzip compressed data. - pub fn from_data( - data: &[u8], - opt: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database, - ) -> Result { + pub fn from_data(data: &[u8], opt: &Options) -> Result { if data.starts_with(&[0x1f, 0x8b]) { let data = decompress_svgz(data)?; let text = std::str::from_utf8(&data).map_err(|_| Error::NotAnUtf8Str)?; - Self::from_str( - text, - opt, - #[cfg(feature = "text")] - fontdb, - ) + Self::from_str(text, opt) } else { let text = std::str::from_utf8(data).map_err(|_| Error::NotAnUtf8Str)?; - Self::from_str( - text, - opt, - #[cfg(feature = "text")] - fontdb, - ) + Self::from_str(text, opt) } } /// Parses `Tree` from an SVG string. - pub fn from_str( - text: &str, - opt: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database, - ) -> Result { + pub fn from_str(text: &str, opt: &Options) -> Result { let xml_opt = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() @@ -134,27 +116,13 @@ impl crate::Tree { let doc = roxmltree::Document::parse_with_options(text, xml_opt).map_err(Error::ParsingFailed)?; - Self::from_xmltree( - &doc, - opt, - #[cfg(feature = "text")] - fontdb, - ) + Self::from_xmltree(&doc, opt) } /// Parses `Tree` from `roxmltree::Document`. - pub fn from_xmltree( - doc: &roxmltree::Document, - opt: &Options, - #[cfg(feature = "text")] fontdb: &fontdb::Database, - ) -> Result { + pub fn from_xmltree(doc: &roxmltree::Document, opt: &Options) -> Result { let doc = svgtree::Document::parse_tree(doc)?; - self::converter::convert_doc( - &doc, - opt, - #[cfg(feature = "text")] - fontdb, - ) + self::converter::convert_doc(&doc, opt) } } diff --git a/crates/usvg/src/parser/options.rs b/crates/usvg/src/parser/options.rs index 77973e321..7b3c51a77 100644 --- a/crates/usvg/src/parser/options.rs +++ b/crates/usvg/src/parser/options.rs @@ -2,11 +2,16 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +#[cfg(feature = "text")] +use std::sync::Arc; + +#[cfg(feature = "text")] +use crate::FontResolver; use crate::{ImageHrefResolver, ImageRendering, ShapeRendering, Size, TextRendering}; /// Processing options. #[derive(Debug)] -pub struct Options { +pub struct Options<'a> { /// Directory that will be used during relative paths resolving. /// /// Expected to be the same as the directory that contains the SVG file, @@ -75,11 +80,26 @@ pub struct Options { /// Specifies the way `xlink:href` in `` elements should be handled. /// /// Default: see type's documentation for details - pub image_href_resolver: ImageHrefResolver, + pub image_href_resolver: ImageHrefResolver<'a>, + + /// Specifies how fonts should be resolved and loaded. + #[cfg(feature = "text")] + pub font_resolver: FontResolver<'a>, + + /// A database of fonts usable by text. + /// + /// This is a base database. If a custom `font_resolver` is specified, + /// additional fonts can be loaded during parsing. Those will be added to a + /// copy of this database. The full database containing all fonts referenced + /// in a `Tree` becomes available as [`Tree::fontdb`](crate::Tree::fontdb) + /// after parsing. If no fonts were loaded dynamically, that database will + /// be the same as this one. + #[cfg(feature = "text")] + pub fontdb: Arc, } -impl Default for Options { - fn default() -> Options { +impl Default for Options<'_> { + fn default() -> Options<'static> { Options { resources_dir: None, dpi: 96.0, @@ -92,11 +112,15 @@ impl Default for Options { image_rendering: ImageRendering::default(), default_size: Size::from_wh(100.0, 100.0).unwrap(), image_href_resolver: ImageHrefResolver::default(), + #[cfg(feature = "text")] + font_resolver: FontResolver::default(), + #[cfg(feature = "text")] + fontdb: Arc::new(fontdb::Database::new()), } } } -impl Options { +impl Options<'_> { /// Converts a relative path into absolute relative to the SVG file itself. /// /// If `Options::resources_dir` is not set, returns itself. @@ -106,4 +130,12 @@ impl Options { None => rel_path.into(), } } + + /// Mutably acquires the database. + /// + /// This clones the database if it is currently shared. + #[cfg(feature = "text")] + pub fn fontdb_mut(&mut self) -> &mut fontdb::Database { + Arc::make_mut(&mut self.fontdb) + } } diff --git a/crates/usvg/src/parser/text.rs b/crates/usvg/src/parser/text.rs index 1da40283b..9c9dc89e6 100644 --- a/crates/usvg/src/parser/text.rs +++ b/crates/usvg/src/parser/text.rs @@ -141,7 +141,7 @@ pub(crate) fn convert( layouted: vec![], }; - if text::convert(&mut text, state.fontdb).is_none() { + if text::convert(&mut text, &state.opt.font_resolver, &mut cache.fontdb).is_none() { return; } diff --git a/crates/usvg/src/text/flatten.rs b/crates/usvg/src/text/flatten.rs index fb4eb3aa7..6539b5c71 100644 --- a/crates/usvg/src/text/flatten.rs +++ b/crates/usvg/src/text/flatten.rs @@ -260,7 +260,7 @@ impl DatabaseExt for Database { self.with_face_data(id, |data, face_index| -> Option { let font = ttf_parser::Face::parse(data, face_index).ok()?; let image = font.glyph_svg_image(glyph_id)?; - Tree::from_data(image.data, &Options::default(), &fontdb::Database::new()).ok() + Tree::from_data(image.data, &Options::default()).ok() })? } @@ -300,12 +300,7 @@ impl DatabaseExt for Database { )?; svg.end_element(); - Tree::from_data( - &svg.end_document().as_bytes(), - &Options::default(), - &fontdb::Database::new(), - ) - .ok() + Tree::from_data(&svg.end_document().as_bytes(), &Options::default()).ok() })? } } diff --git a/crates/usvg/src/text/layout.rs b/crates/usvg/src/text/layout.rs index 801981639..bca766679 100644 --- a/crates/usvg/src/text/layout.rs +++ b/crates/usvg/src/text/layout.rs @@ -11,16 +11,14 @@ use kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; use rustybuzz::ttf_parser; use rustybuzz::ttf_parser::{GlyphId, Tag}; use strict_num::NonZeroPositiveF32; -use svgtypes::FontFamily; use tiny_skia_path::{NonZeroRect, Transform}; use unicode_script::UnicodeScript; use crate::tree::{BBox, IsValidLength}; use crate::{ AlignmentBaseline, ApproxZeroUlps, BaselineShift, DominantBaseline, Fill, FillRule, Font, - FontStretch, FontStyle, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, - TextAnchor, TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, Visibility, - WritingMode, + FontResolver, LengthAdjust, PaintOrder, Path, ShapeRendering, Stroke, Text, TextAnchor, + TextChunk, TextDecorationStyle, TextFlow, TextPath, TextSpan, Visibility, WritingMode, }; /// A glyph that has already been positioned correctly. @@ -45,7 +43,8 @@ pub struct PositionedGlyph { pub id: GlyphId, /// The text from the original string that corresponds to that glyph. pub text: String, - /// The ID of the font the glyph should be taken from. + /// The ID of the font the glyph should be taken from. Can be used with the + /// [font database of the tree](crate::Tree::fontdb) this glyph is part of. pub font: ID, } @@ -206,14 +205,17 @@ impl GlyphCluster { pub(crate) fn layout_text( text_node: &Text, - fontdb: &fontdb::Database, + resolver: &FontResolver, + fontdb: &mut Arc, ) -> Option<(Vec, NonZeroRect)> { let mut fonts_cache: FontsCache = HashMap::new(); for chunk in &text_node.chunks { for span in &chunk.spans { if !fonts_cache.contains_key(&span.font) { - if let Some(font) = resolve_font(&span.font, fontdb) { + if let Some(font) = + (resolver.select_font)(&span.font, fontdb).and_then(|id| fontdb.load_font(id)) + { fonts_cache.insert(span.font.clone(), Arc::new(font)); } } @@ -231,7 +233,7 @@ pub(crate) fn layout_text( TextFlow::Path(_) => (0.0, 0.0), }; - let mut clusters = process_chunk(chunk, &fonts_cache, fontdb); + let mut clusters = process_chunk(chunk, &fonts_cache, resolver, fontdb); if clusters.is_empty() { char_offset += chunk.text.chars().count(); continue; @@ -862,7 +864,8 @@ fn collect_normals( fn process_chunk( chunk: &TextChunk, fonts_cache: &FontsCache, - fontdb: &fontdb::Database, + resolver: &FontResolver, + fontdb: &mut Arc, ) -> Vec { // The way this function works is a bit tricky. // @@ -907,6 +910,7 @@ fn process_chunk( font, span.small_caps, span.apply_kerning, + resolver, fontdb, ); @@ -1183,62 +1187,6 @@ fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphClu } } -fn resolve_font(font: &Font, fontdb: &fontdb::Database) -> Option { - let mut name_list = Vec::new(); - for family in &font.families { - name_list.push(match family { - FontFamily::Serif => fontdb::Family::Serif, - FontFamily::SansSerif => fontdb::Family::SansSerif, - FontFamily::Cursive => fontdb::Family::Cursive, - FontFamily::Fantasy => fontdb::Family::Fantasy, - FontFamily::Monospace => fontdb::Family::Monospace, - FontFamily::Named(s) => fontdb::Family::Name(s), - }); - } - - // Use the default font as fallback. - name_list.push(fontdb::Family::Serif); - - let stretch = match font.stretch { - FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed, - FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed, - FontStretch::Condensed => fontdb::Stretch::Condensed, - FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed, - FontStretch::Normal => fontdb::Stretch::Normal, - FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded, - FontStretch::Expanded => fontdb::Stretch::Expanded, - FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded, - FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded, - }; - - let style = match font.style { - FontStyle::Normal => fontdb::Style::Normal, - FontStyle::Italic => fontdb::Style::Italic, - FontStyle::Oblique => fontdb::Style::Oblique, - }; - - let query = fontdb::Query { - families: &name_list, - weight: fontdb::Weight(font.weight), - stretch, - style, - }; - - let id = fontdb.query(&query); - if id.is_none() { - log::warn!( - "No match for '{}' font-family.", - font.families - .iter() - .map(|f| f.to_string()) - .collect::>() - .join(", ") - ); - } - - fontdb.load_font(id?) -} - pub(crate) trait DatabaseExt { fn load_font(&self, id: ID) -> Option; fn has_char(&self, id: ID, c: char) -> bool; @@ -1336,7 +1284,8 @@ pub(crate) fn shape_text( font: Arc, small_caps: bool, apply_kerning: bool, - fontdb: &fontdb::Database, + resolver: &FontResolver, + fontdb: &mut Arc, ) -> Vec { let mut glyphs = shape_text_with_font(text, font.clone(), small_caps, apply_kerning, fontdb) .unwrap_or_default(); @@ -1355,7 +1304,9 @@ pub(crate) fn shape_text( } if let Some(c) = missing { - let fallback_font = match find_font_for_char(c, &used_fonts, fontdb) { + let fallback_font = match (resolver.select_fallback)(c, &used_fonts, fontdb) + .and_then(|id| fontdb.load_font(id)) + { Some(v) => Arc::new(v), None => break 'outer, }; @@ -1498,55 +1449,6 @@ fn shape_text_with_font( })? } -/// Finds a font with a specified char. -/// -/// This is a rudimentary font fallback algorithm. -fn find_font_for_char( - c: char, - exclude_fonts: &[fontdb::ID], - fontdb: &fontdb::Database, -) -> Option { - let base_font_id = exclude_fonts[0]; - - // Iterate over fonts and check if any of them support the specified char. - for face in fontdb.faces() { - // Ignore fonts, that were used for shaping already. - if exclude_fonts.contains(&face.id) { - continue; - } - - // Check that the new face has the same style. - let base_face = fontdb.face(base_font_id)?; - if base_face.style != face.style - && base_face.weight != face.weight - && base_face.stretch != face.stretch - { - continue; - } - - if !fontdb.has_char(face.id, c) { - continue; - } - - let base_family = base_face - .families - .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) - .unwrap_or(&base_face.families[0]); - - let new_family = face - .families - .iter() - .find(|f| f.1 == fontdb::Language::English_UnitedStates) - .unwrap_or(&base_face.families[0]); - - log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); - return fontdb.load_font(face.id); - } - - None -} - /// An iterator over glyph clusters. /// /// Input: 0 2 2 2 3 4 4 5 5 diff --git a/crates/usvg/src/text/mod.rs b/crates/usvg/src/text/mod.rs index 6420df1d5..10c8a9227 100644 --- a/crates/usvg/src/text/mod.rs +++ b/crates/usvg/src/text/mod.rs @@ -2,7 +2,13 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -use crate::Text; +use std::sync::Arc; + +use fontdb::{Database, ID}; +use svgtypes::FontFamily; + +use self::layout::DatabaseExt; +use crate::{Font, FontStretch, FontStyle, Text}; mod flatten; @@ -10,13 +16,197 @@ mod colr; /// Provides access to the layout of a text node. pub mod layout; +/// A shorthand for [FontResolver]'s font selection function. +/// +/// This function receives a font specification (families + a style, weight, +/// stretch triple) and a font database and should return the ID of the font +/// that shall be used (if any). +/// +/// In the basic case, the function will search the existing fonts in the +/// database to find a good match, e.g. via +/// [`Database::query`](fontdb::Database::query). This is what the [default +/// implementation](FontResolver::default_font_selector) does. +/// +/// Users with more complex requirements can mutate the database to load +/// additional fonts dynamically. To perform mutation, it is recommended to call +/// `Arc::make_mut` on the provided database. (This call is not done outside of +/// the callback to not needless clone an underlying shared database if no +/// mutation will be performed.) It is important that the database is only +/// mutated additively. Removing fonts or replacing the entire database will +/// break things. +pub type FontSelectionFn<'a> = + Box) -> Option + Send + Sync + 'a>; + +/// A shorthand for [FontResolver]'s fallback selection function. +/// +/// This function receives a specific character, a list of already used fonts, +/// and a font database. It should return the ID of a font that +/// - is not any of the already used fonts +/// - is as close as possible to the first already used font (if any) +/// - supports the given character +/// +/// The function can search the existing database, but can also load additional +/// fonts dynamically. See the documentation of [`FontSelectionFn`] for more +/// details. +pub type FallbackSelectionFn<'a> = + Box) -> Option + Send + Sync + 'a>; + +/// A font resolver for `` elements. +/// +/// This type can be useful if you want to have an alternative font handling to +/// the default one. By default, only fonts specified upfront in +/// [`Options::fontdb`](crate::Options::fontdb) will be used. This type allows +/// you to load additional fonts on-demand and customize the font selection +/// process. +pub struct FontResolver<'a> { + /// Resolver function that will be used when selecting a specific font + /// for a generic [`Font`] specification. + pub select_font: FontSelectionFn<'a>, + + /// Resolver function that will be used when selecting a fallback font for a + /// character. + pub select_fallback: FallbackSelectionFn<'a>, +} + +impl Default for FontResolver<'_> { + fn default() -> Self { + FontResolver { + select_font: FontResolver::default_font_selector(), + select_fallback: FontResolver::default_fallback_selector(), + } + } +} + +impl FontResolver<'_> { + /// Creates a default font selection resolver. + /// + /// The default implementation forwards to + /// [`query`](fontdb::Database::query) on the font database specified in the + /// [`Options`](crate::Options). + pub fn default_font_selector() -> FontSelectionFn<'static> { + Box::new(move |font, fontdb| { + let mut name_list = Vec::new(); + for family in &font.families { + name_list.push(match family { + FontFamily::Serif => fontdb::Family::Serif, + FontFamily::SansSerif => fontdb::Family::SansSerif, + FontFamily::Cursive => fontdb::Family::Cursive, + FontFamily::Fantasy => fontdb::Family::Fantasy, + FontFamily::Monospace => fontdb::Family::Monospace, + FontFamily::Named(s) => fontdb::Family::Name(s), + }); + } + + // Use the default font as fallback. + name_list.push(fontdb::Family::Serif); + + let stretch = match font.stretch { + FontStretch::UltraCondensed => fontdb::Stretch::UltraCondensed, + FontStretch::ExtraCondensed => fontdb::Stretch::ExtraCondensed, + FontStretch::Condensed => fontdb::Stretch::Condensed, + FontStretch::SemiCondensed => fontdb::Stretch::SemiCondensed, + FontStretch::Normal => fontdb::Stretch::Normal, + FontStretch::SemiExpanded => fontdb::Stretch::SemiExpanded, + FontStretch::Expanded => fontdb::Stretch::Expanded, + FontStretch::ExtraExpanded => fontdb::Stretch::ExtraExpanded, + FontStretch::UltraExpanded => fontdb::Stretch::UltraExpanded, + }; + + let style = match font.style { + FontStyle::Normal => fontdb::Style::Normal, + FontStyle::Italic => fontdb::Style::Italic, + FontStyle::Oblique => fontdb::Style::Oblique, + }; + + let query = fontdb::Query { + families: &name_list, + weight: fontdb::Weight(font.weight), + stretch, + style, + }; + + let id = fontdb.query(&query); + if id.is_none() { + log::warn!( + "No match for '{}' font-family.", + font.families + .iter() + .map(|f| f.to_string()) + .collect::>() + .join(", ") + ); + } + + id + }) + } + + /// Creates a default font fallback selection resolver. + /// + /// The default implementation searches through the entire `fontdb` + /// to find a font that has the correct style and supports the character. + pub fn default_fallback_selector() -> FallbackSelectionFn<'static> { + Box::new(|c, exclude_fonts, fontdb| { + let base_font_id = exclude_fonts[0]; + + // Iterate over fonts and check if any of them support the specified char. + for face in fontdb.faces() { + // Ignore fonts, that were used for shaping already. + if exclude_fonts.contains(&face.id) { + continue; + } + + // Check that the new face has the same style. + let base_face = fontdb.face(base_font_id)?; + if base_face.style != face.style + && base_face.weight != face.weight + && base_face.stretch != face.stretch + { + continue; + } + + if !fontdb.has_char(face.id, c) { + continue; + } + + let base_family = base_face + .families + .iter() + .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .unwrap_or(&base_face.families[0]); + + let new_family = face + .families + .iter() + .find(|f| f.1 == fontdb::Language::English_UnitedStates) + .unwrap_or(&base_face.families[0]); + + log::warn!("Fallback from {} to {}.", base_family.0, new_family.0); + return Some(face.id); + } + + None + }) + } +} + +impl std::fmt::Debug for FontResolver<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("FontResolver { .. }") + } +} + /// Convert a text into its paths. This is done in two steps: /// 1. We convert the text into glyphs and position them according to the rules specified in the /// SVG specifiation. While doing so, we also calculate the text bbox (which is not based on the /// outlines of a glyph, but instead the glyph metrics as well as decoration spans). /// 2. We convert all of the positioned glyphs into outlines. -pub(crate) fn convert(text: &mut Text, fontdb: &fontdb::Database) -> Option<()> { - let (text_fragments, bbox) = layout::layout_text(text, fontdb)?; +pub(crate) fn convert( + text: &mut Text, + resolver: &FontResolver, + fontdb: &mut Arc, +) -> Option<()> { + let (text_fragments, bbox) = layout::layout_text(text, resolver, fontdb)?; text.layouted = text_fragments; text.bounding_box = bbox.to_rect(); text.abs_bounding_box = bbox.transform(text.abs_transform)?.to_rect(); diff --git a/crates/usvg/src/tree/mod.rs b/crates/usvg/src/tree/mod.rs index b7f2be33d..ea247d458 100644 --- a/crates/usvg/src/tree/mod.rs +++ b/crates/usvg/src/tree/mod.rs @@ -1545,6 +1545,8 @@ pub struct Tree { pub(crate) clip_paths: Vec>, pub(crate) masks: Vec>, pub(crate) filters: Vec>, + #[cfg(feature = "text")] + pub(crate) fontdb: Arc, } impl Tree { @@ -1608,6 +1610,12 @@ impl Tree { &self.filters } + /// Returns the font database that applies to all text nodes in the tree. + #[cfg(feature = "text")] + pub fn fontdb(&self) -> &Arc { + &self.fontdb + } + pub(crate) fn collect_paint_servers(&mut self) { loop_over_paint_servers(&self.root, &mut |paint| match paint { Paint::Color(_) => {} diff --git a/crates/usvg/tests/parser.rs b/crates/usvg/tests/parser.rs index 53d4ce285..0ca5e18d1 100644 --- a/crates/usvg/tests/parser.rs +++ b/crates/usvg/tests/parser.rs @@ -9,8 +9,7 @@ fn clippath_with_invalid_child() { "; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); // clipPath is invalid and should be removed together with rect. assert_eq!(tree.root().has_children(), false); } @@ -23,8 +22,7 @@ fn simplify_paths() { "; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); let path = &tree.root().children()[0]; match path { usvg::Node::Path(ref path) => { @@ -38,8 +36,7 @@ fn simplify_paths() { #[test] fn size_detection_1() { let svg = ""; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.size(), usvg::Size::from_wh(10.0, 20.0).unwrap()); } @@ -47,8 +44,7 @@ fn size_detection_1() { fn size_detection_2() { let svg = ""; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.size(), usvg::Size::from_wh(30.0, 40.0).unwrap()); } @@ -56,8 +52,7 @@ fn size_detection_2() { fn size_detection_3() { let svg = ""; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.size(), usvg::Size::from_wh(5.0, 20.0).unwrap()); } @@ -68,8 +63,7 @@ fn size_detection_4() { "; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.size(), usvg::Size::from_wh(36.0, 36.0).unwrap()); assert_eq!(tree.size(), usvg::Size::from_wh(36.0, 36.0).unwrap()); } @@ -77,16 +71,14 @@ fn size_detection_4() { #[test] fn size_detection_5() { let svg = ""; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.size(), usvg::Size::from_wh(100.0, 100.0).unwrap()); } #[test] fn invalid_size_1() { let svg = ""; - let fontdb = usvg::fontdb::Database::new(); - let result = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb); + let result = usvg::Tree::from_str(&svg, &usvg::Options::default()); assert!(result.is_err()); } @@ -104,8 +96,7 @@ fn path_transform() { "; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.root().children().len(), 1); let group_node = &tree.root().children()[0]; @@ -138,8 +129,7 @@ fn path_transform_nested() { "; - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); assert_eq!(tree.root().children().len(), 1); let group_node1 = &tree.root().children()[0]; @@ -196,8 +186,7 @@ fn path_transform_in_symbol_no_clip() { // // - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); let group_node1 = &tree.root().children()[0]; assert!(matches!(group_node1, usvg::Node::Group(_))); @@ -258,8 +247,7 @@ fn path_transform_in_symbol_with_clip() { // // - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); let group_node1 = &tree.root().children()[0]; assert!(matches!(group_node1, usvg::Node::Group(_))); @@ -326,8 +314,7 @@ fn path_transform_in_svg() { // // - let fontdb = usvg::fontdb::Database::new(); - let tree = usvg::Tree::from_str(&svg, &usvg::Options::default(), &fontdb).unwrap(); + let tree = usvg::Tree::from_str(&svg, &usvg::Options::default()).unwrap(); let group_node1 = &tree.root().children()[0]; assert!(matches!(group_node1, usvg::Node::Group(_))); diff --git a/crates/usvg/tests/write.rs b/crates/usvg/tests/write.rs index 45b2d3b89..1e3821ceb 100644 --- a/crates/usvg/tests/write.rs +++ b/crates/usvg/tests/write.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; + use once_cell::sync::Lazy; -static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { +static GLOBAL_FONTDB: Lazy> = Lazy::new(|| { let mut fontdb = usvg::fontdb::Database::new(); fontdb.load_fonts_dir("../resvg/tests/fonts"); fontdb.set_serif_family("Noto Serif"); @@ -8,7 +10,7 @@ static GLOBAL_FONTDB: Lazy> = Lazy::new fontdb.set_cursive_family("Yellowtail"); fontdb.set_fantasy_family("Sedgwick Ave Display"); fontdb.set_monospace_family("Noto Mono"); - std::sync::Mutex::new(fontdb) + Arc::new(fontdb) }); fn resave(name: &str) { @@ -27,9 +29,11 @@ fn resave_impl(name: &str, id_prefix: Option, preserve_text: bool) { let input_svg = std::fs::read_to_string(format!("tests/files/{}.svg", name)).unwrap(); let tree = { - let fontdb = GLOBAL_FONTDB.lock().unwrap(); - let opt = usvg::Options::default(); - usvg::Tree::from_str(&input_svg, &opt, &fontdb).unwrap() + let opt = usvg::Options { + fontdb: GLOBAL_FONTDB.clone(), + ..Default::default() + }; + usvg::Tree::from_str(&input_svg, &opt).unwrap() }; let mut xml_opt = usvg::WriteOptions::default(); xml_opt.id_prefix = id_prefix; diff --git a/tools/explorer-thumbnailer/src/utils.rs b/tools/explorer-thumbnailer/src/utils.rs index b11c8ef30..53a9e1ecc 100644 --- a/tools/explorer-thumbnailer/src/utils.rs +++ b/tools/explorer-thumbnailer/src/utils.rs @@ -26,12 +26,10 @@ pub unsafe fn tree_from_istream(pstream: LPSTREAM) -> Result } svg_data.set_len(len as usize); - let opt = usvg::Options::default(); + let mut opt = usvg::Options::default(); + opt.fontdb_mut().load_system_fonts(); - let mut fontdb = fontdb::Database::new(); - fontdb.load_system_fonts(); - - let tree = usvg::Tree::from_data(&svg_data, &opt, &fontdb).map_err(|e| Error::TreeError(e))?; + let tree = usvg::Tree::from_data(&svg_data, &opt).map_err(|e| Error::TreeError(e))?; Ok(tree) }