Skip to content

Commit

Permalink
feat: improve custom loaded fonts (#209)
Browse files Browse the repository at this point in the history
* feat: improve custom loaded fonts

* feat: refactoring and adding test cases

When `loadSystemFonts: true`, the `defaultFontFamily` option can also be omitted.

* feat(wasm): support for getting the default font-family from `fontsBuffers`

* chore: new example/text.js

* feat: the defaultFontFamily is not found in the OS and needs to be fallback

Previously, resvg-js would not match any fonts in SVG text, even if there were fonts in the OS that could render them. This commit makes it possible to fallback properly.

```js
const resvg = new Resvg(svg, {
    font: {
      loadSystemFonts: true,
      defaultFontFamily: 'this-is-a-non-existent-font-family',
    },
  })
```

* ci: list Linux font dir

* update CI

* test

* test2

* test3

* test4

* test5 apt-get install fontconfig

* test5: remove fontconfig in CI
  • Loading branch information
yisibl authored Aug 21, 2023
1 parent a7918da commit 8d5cd92
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 86 deletions.
19 changes: 11 additions & 8 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: ['14', '16', '18']
node: ['18']
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
124 changes: 109 additions & 15 deletions __test__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<text fill="blue" font-family="serif" font-size="120">
<tspan x="40" y="143">水</tspan>
</text>
</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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<text fill="blue" font-family="serif" font-size="120">
<tspan x="40" y="143">水</tspan>
</text>
</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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200" viewBox="0 0 300 200">
<text fill="blue" font-family="" font-size="100">
<tspan x="10" y="60%">Abc</tspan>
</text>
</svg>
`
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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="200" viewBox="0 0 300 200">
<text fill="blue" font-family="" font-size="100">
<tspan x="10" y="60%">Abc</tspan>
</text>
</svg>
`
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))
Expand All @@ -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(() => {})
}
Expand Down Expand Up @@ -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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<text fill="blue" font-family="serif" font-size="120">
<tspan x="40" y="143">水</tspan>
</text>
</svg>
`
test('should render using font buffer provided by options', async (t) => {
const svg = `<svg width='480' height='150' viewBox='-20 -80 550 100' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
<text x='0' y='0' font-size='100' fill='#000'>Font Buffer</text>
</svg>`

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) => {
Expand Down
62 changes: 61 additions & 1 deletion __test__/wasm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<text fill="blue" font-family="serif" font-size="120">
<tspan x="40" y="143">水</tspan>
</text>
</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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<text fill="blue" font-family="serif" font-size="120">
<tspan x="40" y="143">水</tspan>
</text>
</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 = `<svg width="200px" height="200px" viewBox="0 0 200 200" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect fill="green" x="0" y="0" width="100" height="100"></rect>
Expand Down Expand Up @@ -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 = `<svg width='480' height='150' viewBox='-20 -80 550 100' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
<text x='0' y='0' font-size='100' font-family='Pacifico' fill='#000000'>Font Buffer</text>
<text x='0' y='0' font-size='100' fill='#000'>Font Buffer</text>
</svg>`

const pacificoBuffer = await fs.readFile(join(__dirname, './Pacifico-Regular.ttf'))
Expand All @@ -339,6 +398,7 @@ test('should render using font buffer provided by options', async (t) => {
const options = {
font: {
fontsBuffers: [pacificoBuffer],
// defaultFontFamily: 'Pacifico',
},
}

Expand Down
Binary file added example/Pacifico-Regular.ttf
Binary file not shown.
22 changes: 14 additions & 8 deletions example/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,30 @@ const { Resvg } = require('../index')

async function main() {
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
<text fill="blue" font-family="serif" font-size="120">
<tspan x="40" y="143">水</tspan>
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="200" viewBox="0 0 500 200">
<defs>
<linearGradient id="fill" x1="0%" y1="45%" x2="100%" y2="55%">
<stop stop-color="#7f00de" offset="0%"/>
<stop stop-color="#ff007e" offset="100%"/>
</linearGradient>
</defs>
<text fill="url(#fill)" font-family="" font-size="60">
<tspan x="40" y="80">竹外桃花三两枝</tspan>
<tspan x="40" y="160">Hello resvg-js</tspan>
</text>
</svg>
`
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)
Expand Down
Binary file modified example/text2-out.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

1 comment on commit 8d5cd92

@vercel
Copy link

@vercel vercel bot commented on 8d5cd92 Aug 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

resvg-js – ./

resvg-js-git-main-yisibl.vercel.app
resvg-js.vercel.app
resvg-js-yisibl.vercel.app

Please sign in to comment.