diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 2c1642af..a8bbbbeb 100755 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -199,7 +199,7 @@ jobs: strategy: fail-fast: false matrix: - node: ['14', '16', '18'] + node: ['18'] runs-on: ubuntu-latest steps: @@ -324,7 +324,7 @@ jobs: run: ls -R . shell: bash - name: Test bindings - run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-slim yarn test + run: docker run --rm -v /usr/share/fonts:/usr/share/fonts -v $(pwd):/build -w /build node:${{ matrix.node }}-slim yarn test test-linux-x64-musl-binding: name: Test bindings on x86_64-unknown-linux-musl - node@${{ matrix.node }} needs: @@ -363,7 +363,7 @@ jobs: run: ls -R . shell: bash - name: Test bindings - run: docker run --rm -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine yarn test + run: docker run --rm -v /usr/share/fonts:/usr/share/fonts -v $(pwd):/build -w /build node:${{ matrix.node }}-alpine yarn test test-linux-aarch64-gnu-binding: name: Test bindings on aarch64-unknown-linux-gnu - node@${{ matrix.node }} needs: @@ -401,11 +401,12 @@ jobs: uses: addnab/docker-run-action@v3 with: image: ghcr.io/napi-rs/napi-rs/nodejs:aarch64-${{ matrix.node }} - options: '-v ${{ github.workspace }}:/build -w /build' + options: '-v /usr/share/fonts:/usr/share/fonts -v ${{ github.workspace }}:/build -w /build' run: | set -e - yarn test + find /usr/share/fonts -name *.ttf ls -la + yarn test test-linux-aarch64-musl-binding: name: Test bindings on aarch64-unknown-linux-musl - node@${{ matrix.node }} needs: @@ -436,9 +437,10 @@ jobs: uses: addnab/docker-run-action@v3 with: image: multiarch/alpine:aarch64-latest-stable - options: '-v ${{ github.workspace }}:/build -w /build' + options: '-v /usr/share/fonts:/usr/share/fonts -v ${{ github.workspace }}:/build -w /build' run: | set -e + find /usr/share/fonts -name *.ttf apk add nodejs npm yarn yarn test test-linux-arm-gnueabihf-binding: @@ -477,11 +479,12 @@ jobs: uses: addnab/docker-run-action@v3 with: image: ghcr.io/napi-rs/napi-rs/nodejs:armhf-${{ matrix.node }} - options: '-v ${{ github.workspace }}:/build -w /build' + options: '-v /usr/share/fonts:/usr/share/fonts -v ${{ github.workspace }}:/build -w /build' run: | set -e - yarn test + find /usr/share/fonts -name *.ttf ls -la + yarn test publish: name: Publish diff --git a/__test__/index.spec.ts b/__test__/index.spec.ts index 7e9f42bc..25df3650 100755 --- a/__test__/index.spec.ts +++ b/__test__/index.spec.ts @@ -224,6 +224,100 @@ test('Load custom font', async (t) => { t.is(result.getHeight(), 687) }) +test('should be load custom fontFiles(no defaultFontFamily option)', (t) => { + const svg = ` + + + + + + ` + const resvg = new Resvg(svg, { + font: { + fontFiles: ['./example/SourceHanSerifCN-Light-subset.ttf'], + loadSystemFonts: false, + // defaultFontFamily: 'Source Han Serif CN Light', + }, + logLevel: 'debug', + }) + const pngData = resvg.render() + const originPixels = pngData.pixels.toJSON().data + + // Find the number of blue `rgb(0,255,255)`pixels + t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) +}) + +test('should be load custom fontDirs(no defaultFontFamily option)', (t) => { + const svg = ` + + + + + + ` + const resvg = new Resvg(svg, { + font: { + fontDirs: ['./example/'], + // loadSystemFonts: false, + // defaultFontFamily: 'Source Han Serif CN Light', + }, + logLevel: 'debug', + }) + const pngData = resvg.render() + const originPixels = pngData.pixels.toJSON().data + + // Find the number of blue `rgb(0,255,255)`pixels + t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) +}) + +test('The defaultFontFamily is not found in the OS and needs to be fallback', (t) => { + const svg = ` + + + Abc + + + ` + const resvg = new Resvg(svg, { + font: { + loadSystemFonts: true, + fontDirs: ['/usr/share/fonts/'], // 防止在 CI 的 Docker 环境找不到字体 + defaultFontFamily: 'this-is-a-non-existent-font-family', + }, + logLevel: 'debug', + }) + const pngData = resvg.render() + const originPixels = pngData.pixels.toJSON().data + // Find the number of blue `rgb(0,255,255)`pixels + const matchPixels = originPixels.join(',').match(/0,0,255/g) + t.true(matchPixels !== null) // If the font is not matched, there are no blue pixels. + t.true((matchPixels?.length ?? 0) > 1500) +}) + +test('Test defaultFontFamily', (t) => { + const svg = ` + + + Abc + + + ` + const resvg = new Resvg(svg, { + font: { + loadSystemFonts: false, + fontDirs: ['./example'], + defaultFontFamily: 'Source Han Serif CN Light', // 指定中文字体,期望自动 fallback 到英文 字体 Pacifico. + }, + logLevel: 'debug', + }) + const pngData = resvg.render() + const originPixels = pngData.pixels.toJSON().data + // Find the number of blue `rgb(0,255,255)`pixels + const matchPixels = originPixels.join(',').match(/0,0,255/g) + t.true(matchPixels !== null) // If the font is not matched, there are no blue pixels. + t.true((matchPixels?.length ?? 0) > 1500) +}) + test('Async rendering', async (t) => { const filePath = '../example/text.svg' const svg = await fs.readFile(join(__dirname, filePath)) @@ -249,7 +343,7 @@ test('Async rendering', async (t) => { const MaybeTest = typeof AbortController !== 'undefined' ? test : test.skip MaybeTest('should be able to abort queued async rendering', async (t) => { - // fill the task queue + // Fill the task queue for (const _ of Array.from({ length: 100 })) { process.nextTick(() => {}) } @@ -449,27 +543,27 @@ test('should return undefined if bbox is invalid', (t) => { t.is(resvg.innerBBox(), undefined) }) -test('should be load custom fonts', (t) => { - const svg = ` - - - - - - ` +test('should render using font buffer provided by options', async (t) => { + const svg = ` + Font Buffer + ` + + const expectedResultBuffer = await fs.readFile(join(__dirname, './options_font_buffer_expected_result.png')) + const resvg = new Resvg(svg, { font: { - fontFiles: ['./example/SourceHanSerifCN-Light-subset.ttf'], + fontFiles: ['./__test__/Pacifico-Regular.ttf'], loadSystemFonts: false, - defaultFontFamily: 'Source Han Serif CN Light', + defaultFontFamily: '', }, logLevel: 'debug', }) - const pngData = resvg.render() - const originPixels = pngData.pixels.toJSON().data + const renderedResult = resvg.render().asPng() - // Find the number of blue `rgb(0,255,255)`pixels - t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) + const expectedResult = await jimp.read(Buffer.from(expectedResultBuffer.buffer)) + const actualPng = await jimp.read(Buffer.from(renderedResult)) + + t.is(jimp.diff(expectedResult, actualPng, 0.01).percent, 0) // 0 means similar, 1 means not similar }) test('should throw because invalid SVG attribute (width attribute is 0)', (t) => { diff --git a/__test__/wasm.spec.ts b/__test__/wasm.spec.ts index 4e894967..09c35711 100755 --- a/__test__/wasm.spec.ts +++ b/__test__/wasm.spec.ts @@ -206,6 +206,65 @@ test('Set the background without alpha by hsla()', async (t) => { t.is(result.hasAlpha(), false) }) +test('Load custom font(use fontsBuffers option)', async (t) => { + const filePath = '../example/text.svg' + const svg = await fs.readFile(join(__dirname, filePath)) + const fontBuffer = await fs.readFile(join(__dirname, '../example/SourceHanSerifCN-Light-subset.ttf')) + const resvg = new Resvg(svg.toString('utf-8'), { + font: { + fontsBuffers: [fontBuffer], // Load custom fonts. + }, + }) + const pngBuffer = resvg.render().asPng() + const result = await jimp.read(Buffer.from(pngBuffer)) + + t.is(result.getWidth(), 1324) + t.is(result.getHeight(), 687) +}) + +test('should be load custom font(no defaultFontFamily option)', async (t) => { + const svg = ` + + + + + + ` + const fontBuffer = await fs.readFile(join(__dirname, '../example/SourceHanSerifCN-Light-subset.ttf')) + const resvg = new Resvg(svg, { + font: { + fontsBuffers: [fontBuffer], + // defaultFontFamily: 'Source Han Serif CN Light', + }, + }) + const pngData = resvg.render() + const originPixels = Array.from(pngData.pixels) + + // Find the number of blue `rgb(0,255,255)`pixels + t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) +}) + +test('should be load custom fontsBuffers(no defaultFontFamily option)', async (t) => { + const svg = ` + + + + + + ` + const fontBuffer = await fs.readFile(join(__dirname, '../example/SourceHanSerifCN-Light-subset.ttf')) + const resvg = new Resvg(svg, { + font: { + fontsBuffers: [fontBuffer], + }, + }) + const pngData = resvg.render() + const originPixels = Array.from(pngData.pixels) + + // Find the number of blue `rgb(0,255,255)`pixels + t.is(originPixels.join(',').match(/0,0,255/g)?.length, 1726) +}) + test('should generate a 80x80 png and opaque', async (t) => { const svg = ` @@ -330,7 +389,7 @@ test('should return undefined if bbox is invalid', (t) => { test('should render using font buffer provided by options', async (t) => { const svg = ` - Font Buffer + Font Buffer ` const pacificoBuffer = await fs.readFile(join(__dirname, './Pacifico-Regular.ttf')) @@ -339,6 +398,7 @@ test('should render using font buffer provided by options', async (t) => { const options = { font: { fontsBuffers: [pacificoBuffer], + // defaultFontFamily: 'Pacifico', }, } diff --git a/example/Pacifico-Regular.ttf b/example/Pacifico-Regular.ttf new file mode 100644 index 00000000..e7def95d Binary files /dev/null and b/example/Pacifico-Regular.ttf differ diff --git a/example/text.js b/example/text.js index 29104e98..d407627a 100644 --- a/example/text.js +++ b/example/text.js @@ -6,24 +6,30 @@ const { Resvg } = require('../index') async function main() { const svg = ` - - - + + + + + + + + + 竹外桃花三两枝 + Hello resvg-js ` const t = performance.now() const resvg = new Resvg(svg, { - background: 'pink', + background: '#fff', 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', + // fontFiles: ['./__test__/Pacifico-Regular.ttf'], + fontDirs: ['./example'], }, logLevel: 'debug', // Default Value: error }) - const pngData = resvg.render() - const pngBuffer = pngData.asPng() + const pngBuffer = resvg.render().asPng() console.info('✨ Done in', performance.now() - t, 'ms') await promises.writeFile(join(__dirname, './text2-out.png'), pngBuffer) diff --git a/example/text2-out.png b/example/text2-out.png index 0fed6e5e..9e6f60aa 100644 Binary files a/example/text2-out.png and b/example/text2-out.png differ diff --git a/src/fonts.rs b/src/fonts.rs index c7bfa46c..3200bcd3 100644 --- a/src/fonts.rs +++ b/src/fonts.rs @@ -3,7 +3,7 @@ // file, You can obtain one at http://mozilla.org/MPL/2.0/. use crate::options::*; -use resvg::usvg_text_layout::fontdb::Database; +use resvg::usvg_text_layout::fontdb::{Database, Language}; #[cfg(not(target_arch = "wasm32"))] use log::{debug, warn}; @@ -21,12 +21,6 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database { let mut fontdb = Database::new(); let now = std::time::Instant::now(); - // 加载系统字体 - // https://github.com/RazrFalcon/fontdb/blob/052d74b9eb45f2c4f446846a53f33bd965e2662d/src/lib.rs#L261 - if font_options.load_system_fonts { - fontdb.load_system_fonts(); - } - // 加载指定路径的字体 for path in &font_options.font_files { if let Err(e) = fontdb.load_font_file(path) { @@ -39,6 +33,13 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database { fontdb.load_fonts_dir(path); } + // 加载系统字体 + // 放到最后加载,这样在获取 default_font_family 时才能优先读取到自定义的字体。 + // https://github.com/RazrFalcon/fontdb/blob/052d74b9eb45f2c4f446846a53f33bd965e2662d/src/lib.rs#L261 + if font_options.load_system_fonts { + fontdb.load_system_fonts(); + } + set_font_families(font_options, &mut fontdb); debug!( @@ -47,15 +48,128 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database { now.elapsed().as_micros() as f64 / 1000.0 ); - // 查找指定字体的路径 - let font_family: &str = &font_options.default_font_family; + fontdb +} + +/// Loads fonts in Wasm. +#[cfg(target_arch = "wasm32")] +pub fn load_wasm_fonts( + font_options: &JsFontOptions, + fonts_buffers: Option, + fontdb: &mut Database, +) -> Result<(), js_sys::Error> { + if let Some(ref fonts_buffers) = fonts_buffers { + for font in fonts_buffers.values().into_iter() { + let raw_font = font?; + let font_data = raw_font.dyn_into::()?.to_vec(); + fontdb.load_font_data(font_data); + } + } + + set_wasm_font_families(font_options, fontdb, fonts_buffers); + + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn set_font_families(font_options: &JsFontOptions, fontdb: &mut Database) { + let mut default_font_family = font_options.default_font_family.clone().trim().to_string(); + // Debug: get font lists + // for face in fontdb.faces() { + // let family = face + // .families + // .iter() + // .find(|f| f.1 == Language::English_UnitedStates) + // .unwrap_or(&face.families[0]); + // debug!("font_id = {}, family_name = {}", face.id, family.0); + // } + + let fontdb_found_default_font_family = fontdb + .faces() + .iter() + .find_map(|it| { + it.families + .iter() + .find(|f| f.0 == default_font_family) + .map(|f| f.0.clone()) + }) + .unwrap_or_default(); + + // 当 default_font_family 为空或系统无该字体时,尝试把 fontdb + // 中字体列表的第一个字体设置为默认的字体。 + if default_font_family.is_empty() || fontdb_found_default_font_family.is_empty() { + // font_files 或 font_dirs 选项不为空时, 从已加载的字体列表中获取第一个字体的 font family。 + if !font_options.font_files.is_empty() || !font_options.font_dirs.is_empty() { + default_font_family = get_first_font_family_or_fallback(fontdb); + } + } + + fontdb.set_serif_family(&default_font_family); + fontdb.set_sans_serif_family(&default_font_family); + fontdb.set_cursive_family(&default_font_family); + fontdb.set_fantasy_family(&default_font_family); + fontdb.set_monospace_family(&default_font_family); + + debug!("📝 default_font_family = '{}'", default_font_family); + + #[cfg(not(target_arch = "wasm32"))] + find_and_debug_font_path(fontdb, default_font_family.as_str()) +} + +#[cfg(target_arch = "wasm32")] +fn set_wasm_font_families( + font_options: &JsFontOptions, + fontdb: &mut Database, + fonts_buffers: Option, +) { + let mut default_font_family = font_options.default_font_family.clone(); + let fallback_font_family = "Arial".to_string(); // 其他情况都 fallback 到指定的这个字体。 + + // 当默认字体为空时,尝试直接从 font_files 中加载读取字体名称,然后设置到默认的 font-family 中 + if font_options + .default_font_family + .to_string() + .trim() + .is_empty() + { + if let Some(_fonts_buffers) = fonts_buffers { + // 获取字体列表中第一个字体的 font family。 + match fontdb.faces().iter().next() { + Some(face) => { + let new_family = face + .families + .iter() + .find(|f| f.1 == Language::English_UnitedStates) + .unwrap_or(&face.families[0]); + + default_font_family = new_family.0.clone(); + } + None => { + default_font_family = fallback_font_family; + } + } + } else { + default_font_family = fallback_font_family; + } + } + + fontdb.set_serif_family(&default_font_family); + fontdb.set_sans_serif_family(&default_font_family); + fontdb.set_cursive_family(&default_font_family); + fontdb.set_fantasy_family(&default_font_family); + fontdb.set_monospace_family(&default_font_family); +} + +/// 查询指定 font family 的字体是否存在,如果不存在则使用 fallback_font_family 代替。 +#[cfg(not(target_arch = "wasm32"))] +fn find_and_debug_font_path(fontdb: &mut Database, font_family: &str) { let query = Query { families: &[Family::Name(font_family)], ..Query::default() }; let now = std::time::Instant::now(); - // 当前使用的字体是否存在 + // 查询当前使用的字体是否存在 match fontdb.query(&query) { Some(id) => { let (src, index) = fontdb.face_source(id).unwrap(); @@ -69,53 +183,44 @@ pub fn load_fonts(font_options: &JsFontOptions) -> Database { } } None => { - warn!("Warning: The default font '{}' not found.", font_family); + let first_font_family = get_first_font_family_or_fallback(fontdb); + + fontdb.set_serif_family(&first_font_family); + fontdb.set_sans_serif_family(&first_font_family); + fontdb.set_cursive_family(&first_font_family); + fontdb.set_fantasy_family(&first_font_family); + fontdb.set_monospace_family(&first_font_family); + + warn!( + "Warning: The default font-family '{}' not found, set to '{}'.", + font_family, first_font_family, + ); } } - - fontdb } -/// Loads fonts. -#[cfg(target_arch = "wasm32")] -pub fn load_fonts( - font_options: &JsFontOptions, - fonts_buffers: Option, - fontdb: &mut Database, -) -> Result<(), js_sys::Error> { - if let Some(fonts_buffers) = fonts_buffers { - for font in fonts_buffers.values().into_iter() { - let raw_font = font?; - let font_data = raw_font.dyn_into::()?.to_vec(); - fontdb.load_font_data(font_data); +/// 获取 fontdb 中的第一个字体的 font family。 +#[cfg(not(target_arch = "wasm32"))] +fn get_first_font_family_or_fallback(fontdb: &mut Database) -> String { + let mut default_font_family = "Arial".to_string(); // 其他情况都 fallback 到指定的这个字体。 + + match fontdb.faces().iter().next() { + Some(face) => { + let base_family = face + .families + .iter() + .find(|f| f.1 == Language::English_UnitedStates) + .unwrap_or(&face.families[0]); + + default_font_family = base_family.0.clone(); + } + None => { + debug!( + "📝 get_first_font_family not found = '{}'", + default_font_family + ); } } - set_font_families(font_options, fontdb); - - Ok(()) -} - -fn set_font_families(font_options: &JsFontOptions, fontdb: &mut Database) { - // Set generic font families - // - `serif` - Times New Roman - // - `sans-serif` - Arial - // - `cursive` - Comic Sans MS - // - `fantasy` - Impact (Papyrus on macOS) - // - `monospace` - Courier New - if !font_options.default_font_family.is_empty() { - // If a default font family exists, set all other families to that family. - // This prevents fonts from not being rendered in SVG. - fontdb.set_serif_family(&font_options.default_font_family); - fontdb.set_sans_serif_family(&font_options.default_font_family); - fontdb.set_cursive_family(&font_options.default_font_family); - fontdb.set_fantasy_family(&font_options.default_font_family); - fontdb.set_monospace_family(&font_options.default_font_family); - } else { - fontdb.set_serif_family(&font_options.serif_family); - fontdb.set_sans_serif_family(&font_options.sans_serif_family); - fontdb.set_cursive_family(&font_options.cursive_family); - fontdb.set_fantasy_family(&font_options.fantasy_family); - fontdb.set_monospace_family(&font_options.monospace_family); - } + default_font_family } diff --git a/src/lib.rs b/src/lib.rs index 45f6f8db..73c339eb 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,7 +288,7 @@ impl Resvg { let (mut opts, mut fontdb) = js_options.to_usvg_options(); - crate::fonts::load_fonts(&js_options.font, custom_font_buffers, &mut fontdb)?; + crate::fonts::load_wasm_fonts(&js_options.font, custom_font_buffers, &mut fontdb)?; options::tweak_usvg_options(&mut opts); let mut tree = if js_sys::Uint8Array::instanceof(&svg) { diff --git a/src/options.rs b/src/options.rs index d48e0606..cbe31801 100644 --- a/src/options.rs +++ b/src/options.rs @@ -262,7 +262,7 @@ impl Default for JsFontOptions { load_system_fonts: true, font_files: vec![], font_dirs: vec![], - default_font_family: "Arial".to_string(), + default_font_family: "".to_string(), default_font_size: 12.0, serif_family: "Times New Roman".to_string(), sans_serif_family: "Arial".to_string(), diff --git a/wasm/index_bg.wasm b/wasm/index_bg.wasm index 067d984e..1a581c53 100644 Binary files a/wasm/index_bg.wasm and b/wasm/index_bg.wasm differ