From 7ce506827f5e4f029cb5c743c807a86f6ae9c4c2 Mon Sep 17 00:00:00 2001 From: iola1999 Date: Fri, 22 Nov 2024 03:07:09 +0800 Subject: [PATCH 1/8] fix: fontdb io issue --- index.d.ts | 1 + js-binding.d.ts | 6 +++--- src/fonts.rs | 46 +++++++++++++++++++++++++++++++++++++--------- src/options.rs | 7 +++++++ 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index ce3ca848..ac2d6975 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/js-binding.d.ts b/js-binding.d.ts index c51bd166..2f89e792 100644 --- a/js-binding.d.ts +++ b/js-binding.d.ts @@ -4,13 +4,13 @@ /* auto-generated by NAPI-RS */ export function renderAsync(svg: string | Buffer, options?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise -export class BBox { +export declare class BBox { x: number y: number width: number height: number } -export class Resvg { +export declare class Resvg { constructor(svg: string | Buffer, options?: string | undefined | null) /** Renders an SVG in Node.js */ render(): RenderedImage @@ -40,7 +40,7 @@ export class Resvg { /** Get the SVG height */ get height(): number } -export class RenderedImage { +export declare class RenderedImage { /** Write the image data to Buffer */ asPng(): Buffer /** Get the RGBA pixels of the image */ diff --git a/src/fonts.rs b/src/fonts.rs index 6ccd83f9..78265bde 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -20,20 +20,48 @@ use woff2::decode::{convert_woff2_to_ttf, is_woff2}; /// 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 { + // 预加载模式: 一次性读取所有字体文件到内存 + for path in &font_options.font_files { + match std::fs::read(path) { + Ok(buffer) => { + let _ = fontdb.load_font_data(buffer); + } + Err(e) => { + warn!("Failed to read font file '{}' cause {}.", path, e); + } + } + } + + // 加载字体目录 + for dir in &font_options.font_dirs { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + if let Ok(path) = entry.path().canonicalize() { + if path.is_file() { + if let Ok(buffer) = std::fs::read(&path) { + 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 4a8d2c57..c377c8a7 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(), From 6dfe87333471e4ff9d11bf3dc8e96c4289073419 Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 15:15:28 +0800 Subject: [PATCH 2/8] ci: lint --- src/options.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/options.rs b/src/options.rs index c377c8a7..48c5787a 100644 --- a/src/options.rs +++ b/src/options.rs @@ -240,7 +240,6 @@ pub struct JsFontOptions { /// 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, From 69b34fc9b24cc0a86a02f9e567c8361796a2473d Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 15:53:38 +0800 Subject: [PATCH 3/8] ci: lint --- src/options.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/options.rs b/src/options.rs index 48c5787a..f2d00d0c 100644 --- a/src/options.rs +++ b/src/options.rs @@ -240,6 +240,7 @@ pub struct JsFontOptions { /// 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, From cd4b519540aae1a1afa3919dd11bd00cbe271953 Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 16:01:33 +0800 Subject: [PATCH 4/8] chore: use iterator chain calls --- src/fonts.rs | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/fonts.rs b/src/fonts.rs index 78265bde..aa12cff7 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -17,6 +17,16 @@ use wasm_bindgen::JsCast; #[cfg(target_arch = "wasm32")] use woff2::decode::{convert_woff2_to_ttf, is_woff2}; +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 { @@ -24,30 +34,19 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database { let now = std::time::Instant::now(); if font_options.preload_fonts { - // 预加载模式: 一次性读取所有字体文件到内存 - for path in &font_options.font_files { - match std::fs::read(path) { - Ok(buffer) => { - let _ = fontdb.load_font_data(buffer); - } - Err(e) => { - warn!("Failed to read font file '{}' cause {}.", path, e); - } - } - } + // 预加载单个字体文件 + 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) { - for entry in entries.flatten() { - if let Ok(path) = entry.path().canonicalize() { - if path.is_file() { - if let Ok(buffer) = std::fs::read(&path) { - let _ = fontdb.load_font_data(buffer); - } - } - } - } + 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 { From 61973dd42b8eac0a8d8c0bb9c420587a5c2c555d Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 16:35:19 +0800 Subject: [PATCH 5/8] ci: lint --- src/fonts.rs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/fonts.rs b/src/fonts.rs index aa12cff7..f3b4ce0d 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -21,7 +21,11 @@ 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); + warn!( + "Failed to read font file '{}' cause {}.", + path.as_ref().display(), + e + ); None } } @@ -35,18 +39,25 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database { if font_options.preload_fonts { // 预加载单个字体文件 - font_options.font_files.iter() + font_options + .font_files + .iter() .filter_map(load_font_buffer) - .for_each(|buffer| { let _ = fontdb.load_font_data(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) + 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); }); + .for_each(|buffer| { + let _ = fontdb.load_font_data(buffer); + }); } } } else { From 5ff930a034b36cde51d1489b70e1b568c02c9c2f Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 16:36:08 +0800 Subject: [PATCH 6/8] ci: lint --- js-binding.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js-binding.d.ts b/js-binding.d.ts index 2f89e792..c51bd166 100644 --- a/js-binding.d.ts +++ b/js-binding.d.ts @@ -4,13 +4,13 @@ /* auto-generated by NAPI-RS */ export function renderAsync(svg: string | Buffer, options?: string | undefined | null, signal?: AbortSignal | undefined | null): Promise -export declare class BBox { +export class BBox { x: number y: number width: number height: number } -export declare class Resvg { +export class Resvg { constructor(svg: string | Buffer, options?: string | undefined | null) /** Renders an SVG in Node.js */ render(): RenderedImage @@ -40,7 +40,7 @@ export declare class Resvg { /** Get the SVG height */ get height(): number } -export declare class RenderedImage { +export class RenderedImage { /** Write the image data to Buffer */ asPng(): Buffer /** Get the RGBA pixels of the image */ From 5370450dee1c72ef8826495b960ba96f40f8df15 Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 16:53:54 +0800 Subject: [PATCH 7/8] fix: wasm build --- src/fonts.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fonts.rs b/src/fonts.rs index f3b4ce0d..5ade7e73 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -17,6 +17,7 @@ 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), From 204222f696544b0fbb9f5205cbebb43a4fd1e640 Mon Sep 17 00:00:00 2001 From: iola1999 Date: Mon, 25 Nov 2024 17:54:14 +0800 Subject: [PATCH 8/8] test: add test case for preloadFonts --- __test__/index.spec.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 6a8be607..480bf900 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 +})