diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 6a8be60..480bf90 100755 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -5,7 +5,7 @@ import test from 'ava' import jimp from 'jimp-compact' import fetch from 'node-fetch' -import { Resvg, renderAsync } from '../index' +import { Resvg, ResvgRenderOptions, renderAsync } from '../index' import { jimpToRgbaPixels } from './helper' @@ -704,3 +704,39 @@ test('should throw (SVG string is empty)', (t) => { t.is(error.code, 'GenericFailure') t.is(error.message, 'SVG data parsing failed cause the document does not have a root node') }) + +// issue: https://github.com/thx/resvg-js/issues/367 +test('Load custom font with preload_fonts', async (t) => { + const filePath = '../example/text.svg' + const svg = await fs.readFile(join(__dirname, filePath)) + const svgString = svg.toString('utf-8') + const options: ResvgRenderOptions = { + font: { + fontFiles: ['./example/SourceHanSerifCN-Light-subset.ttf'], // Load custom fonts. + loadSystemFonts: false, // It will be faster to disable loading system fonts. + defaultFontFamily: 'Source Han Serif CN Light', + preloadFonts: false, + }, + } + const expectResvg = new Resvg(svgString, options) + const expectPngData = expectResvg.render() + const expectPngBuffer = expectPngData.asPng() + const expectedResult = await jimp.read(Buffer.from(expectPngBuffer)) + + // enable preloadFonts + options.font!.preloadFonts = true + const actualResvg = new Resvg(svgString, options) + const actualPngData = actualResvg.render() + const actualPngBuffer = actualPngData.asPng() + const actualPng = await jimp.read(Buffer.from(actualPngBuffer)) + t.is(jimp.diff(expectedResult, actualPng, 0.01).percent, 0) // 0 means similar, 1 means not similar + + // use fontDirs + delete options.font!.fontFiles + options.font!.fontDirs = ['./example'] + const actualResvg2 = new Resvg(svgString, options) + const actualPngData2 = actualResvg2.render() + const actualPngBuffer2 = actualPngData2.asPng() + const actualPng2 = await jimp.read(Buffer.from(actualPngBuffer2)) + t.is(jimp.diff(expectedResult, actualPng2, 0.01).percent, 0) // 0 means similar, 1 means not similar +}) diff --git a/index.d.ts b/index.d.ts index ce3ca84..ac2d697 100755 --- a/index.d.ts +++ b/index.d.ts @@ -5,6 +5,7 @@ export type ResvgRenderOptions = { loadSystemFonts?: boolean // Default: true, if set to false, it will be faster. fontFiles?: string[] // A list of local font file paths to load. fontDirs?: string[] // A list of local font directories to load. + preloadFonts?: boolean // Default: false. When enabled, font files will be read into memory at once to avoid frequent IO. defaultFontSize?: number // Default: 12 defaultFontFamily?: string // Default: "", if `loadSystemFonts` is enabled, it will be set to the first font in the list of system fonts. serifFamily?: string diff --git a/src/fonts.rs b/src/fonts.rs index 6ccd83f..5ade7e7 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -17,23 +17,62 @@ use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] use woff2::decode::{convert_woff2_to_ttf, is_woff2}; +#[cfg(not(target_arch = "wasm32"))] +fn load_font_buffer(path: impl AsRef) -> Option> { + match std::fs::read(path.as_ref()) { + Ok(buffer) => Some(buffer), + Err(e) => { + warn!( + "Failed to read font file '{}' cause {}.", + path.as_ref().display(), + e + ); + None + } + } +} + /// 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(); let now = std::time::Instant::now(); - // 加载指定路径的字体 - for path in &font_options.font_files { - if let Err(e) = fontdb.load_font_file(path) { - warn!("Failed to load '{}' cause {}.", path, e); + if font_options.preload_fonts { + // 预加载单个字体文件 + font_options + .font_files + .iter() + .filter_map(load_font_buffer) + .for_each(|buffer| { + let _ = fontdb.load_font_data(buffer); + }); + + // 预加载字体目录 + for dir in &font_options.font_dirs { + if let Ok(entries) = std::fs::read_dir(dir) { + entries + .filter_map(Result::ok) + .filter_map(|entry| entry.path().canonicalize().ok()) + .filter(|path| path.is_file()) + .filter_map(load_font_buffer) + .for_each(|buffer| { + let _ = fontdb.load_font_data(buffer); + }); + } + } + } else { + // 默认模式: 直接传递文件路径 + for path in &font_options.font_files { + if let Err(e) = fontdb.load_font_file(path) { + warn!("Failed to load '{}' cause {}.", path, e); + } } - } - // Load font directories - for path in &font_options.font_dirs { - fontdb.load_fonts_dir(path); + // Load font directories + for path in &font_options.font_dirs { + fontdb.load_fonts_dir(path); + } } // 加载系统字体 diff --git a/src/options.rs b/src/options.rs index 4a8d2c5..f2d00d0 100644 --- a/src/options.rs +++ b/src/options.rs @@ -238,6 +238,12 @@ pub struct JsFontOptions { /// A list of local font directories to load. pub font_dirs: Vec, + /// Whether to preload font files into memory to improve performance. + /// When enabled, font files will be read into memory at once to avoid frequent IO. + /// + /// Default: false + pub preload_fonts: bool, + /// The default font family. /// /// Will be used when no `font-family` attribute is set in the SVG. @@ -284,6 +290,7 @@ impl Default for JsFontOptions { load_system_fonts: true, font_files: vec![], font_dirs: vec![], + preload_fonts: false, default_font_family: "".to_string(), default_font_size: 12.0, serif_family: "Times New Roman".to_string(),