├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── test └── index.js └── testdata ├── Roboto-400.ttf ├── Roboto-400.woff └── Roboto-400.woff2 /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /testdata/ 2 | /node_modules/ 3 | /coverage/ 4 | /.nyc_output/ 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["standard", "prettier", "prettier/standard"], 3 | "plugins": ["import", "mocha"], 4 | "env": { 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "prefer-template": "error", 9 | "mocha/no-exclusive-tests": "error", 10 | "mocha/no-nested-tests": "error", 11 | "mocha/no-identical-title": "error", 12 | "prefer-const": [ 13 | "error", 14 | { 15 | "destructuring": "all", 16 | "ignoreReadBeforeAssign": false 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 'on': 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-18.04 9 | name: Node ${{ matrix.node }} 10 | strategy: 11 | matrix: 12 | node: 13 | - '10' 14 | - '12' 15 | - '14' 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Setup node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node }} 22 | - run: npm install 23 | - run: npm test 24 | 25 | test-targets: 26 | runs-on: ubuntu-18.04 27 | name: ${{ matrix.targets.name }} 28 | strategy: 29 | matrix: 30 | targets: 31 | - name: 'Lint' 32 | target: 'lint' 33 | - name: 'Coverage' 34 | target: 'coverage' 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Setup node 39 | uses: actions/setup-node@v1 40 | with: 41 | node-version: '14' 42 | - run: npm install 43 | - run: npm run ${{ matrix.targets.target }} 44 | - name: Upload coverage 45 | uses: coverallsapp/github-action@master 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} 48 | if: ${{ matrix.targets.target == 'coverage' }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage/ 3 | /.nyc_output/ 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = false 2 | package-lock = false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.nyc_output/ 3 | /coverage/ 4 | /testdata/ 5 | 6 | # Don't fight npm i --save 7 | /package.json 8 | 9 | # Generated 10 | /CHANGELOG.md 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v2.0.0 (2021-04-19) 2 | 3 | - [truetype => sfnt \(semver-major\)](https://github.com/papandreou/fontverter/commit/4b5473931b8b715c83767f75c93a9f76aef63dcb) ([Andreas Lind](mailto:andreas.lind@workday.com)) 4 | 5 | ### v1.0.1 (2021-04-04) 6 | 7 | - [Update wawoff2 to ^2.0.0](https://github.com/papandreou/fontverter/commit/e71ed96b69ba646633fc0255e1e00f0a1a5112a2) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 8 | - [README: Link to the changelog](https://github.com/papandreou/fontverter/commit/5d30f6ead542c366eb8ddaee4823fa3b470dfe50) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 9 | - [README: Mention and link to the license](https://github.com/papandreou/fontverter/commit/a25ca9e14163253d8e080a9d96d63d6b24de87b6) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 10 | 11 | ### v1.0.0 (2021-02-06) 12 | 13 | - [Add a quick README](https://github.com/papandreou/fontverter/commit/01c716373100013f54b1dac69720c84f09959373) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 14 | - [Test conversion to and from unsupported formats](https://github.com/papandreou/fontverter/commit/5221a19d0d5d65eee08bd99a4d127c2d911f012e) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 15 | - [Improve error messages](https://github.com/papandreou/fontverter/commit/17617de3d1553af5abdf684e69117e0f0fc9b315) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 16 | - [Initial commit](https://github.com/papandreou/fontverter/commit/221abe719b529a2043a46108daf13361084ef50d) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Andreas Lind Petersen 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of the author nor the names of contributors may 15 | be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fontverter 2 | 3 | Convert Buffer instances back and forth between SFNT (TrueType/OpenType), WOFF, and WOFF2 formats. 4 | 5 | Uses the WebAssembly-based [`wawoff2`](https://github.com/fontello/wawoff2) and [`woff2sfnt-sfnt2woff`](https://github.com/odemiral/woff2sfnt-sfnt2woff) modules under the hood, so no binaries or native modules are involved. 6 | 7 | ```js 8 | const fontverter = require('fontverter'); 9 | 10 | const mySfntFontBuffer = Buffer.from(/*...*/); 11 | const myWoffFontBuffer = await fontverter.convert(mySfntFontBuffer, 'woff'); 12 | const myWoff2FontBuffer = await fontverter.convert(myWoffFontBuffer, 'woff2'); 13 | ``` 14 | 15 | ## API 16 | 17 | #### `fontverter.convert(buffer, toFormat[, fromFormat]): Promise` 18 | 19 | Asynchronously convert a Buffer instance containing a font to another format. 20 | 21 | `toFormat` and `fromFormat` can be either `'sfnt'`, `'woff'`, or `'woff2'`. 22 | 23 | If `fromFormat` is omitted, the source format will be detected based on the signature at the start of the buffer. 24 | 25 | For backwards compatibility reasons, `'truetype'` is supported as an alias for `'sfnt'` in both `toFormat` and `fromFormat`. 26 | 27 | Returns a promise that is fulfilled with the converted font as a Buffer instance, or rejected with an error. 28 | If `toFormat` is the same as the current format (as specified by `fromFormat` or detected), the original buffer instance will be returned without any conversion taking place. 29 | 30 | #### `fontverter.detectFormat(buffer): String` 31 | 32 | Returns the detected format of the font contained in `buffer` (either `'sfnt'`, `'woff'`, or `'woff2'`), or throws an exception if the format could not be detected. 33 | 34 | ## Releases 35 | 36 | [Changelog](https://github.com/papandreou/fontverter/blob/master/CHANGELOG.md) 37 | 38 | ## License 39 | 40 | 3-clause BSD license -- see the `LICENSE` file for details. 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const wawoff2 = require('wawoff2'); 2 | const woffTool = require('woff2sfnt-sfnt2woff'); 3 | 4 | const supportedFormats = new Set(['sfnt', 'woff', 'woff2']); 5 | 6 | exports.detectFormat = function (buffer) { 7 | const signature = buffer.toString('ascii', 0, 4); 8 | if (signature === 'wOFF') { 9 | return 'woff'; 10 | } else if (signature === 'wOF2') { 11 | return 'woff2'; 12 | } else if ( 13 | signature === 'true' || 14 | signature === 'OTTO' || 15 | signature === '\x00\x01\x00\x00' 16 | ) { 17 | return 'sfnt'; 18 | } else { 19 | throw new Error(`Unrecognized font signature: ${signature}`); 20 | } 21 | }; 22 | 23 | exports.convert = async function (buffer, toFormat, fromFormat) { 24 | if (toFormat === 'truetype') { 25 | toFormat = 'sfnt'; 26 | } 27 | if (fromFormat === 'truetype') { 28 | fromFormat = 'sfnt'; 29 | } 30 | if (!supportedFormats.has(toFormat)) { 31 | throw new Error(`Unsupported target format: ${toFormat}`); 32 | } 33 | if (fromFormat) { 34 | if (!supportedFormats.has(fromFormat)) { 35 | throw new Error(`Unsupported source format: ${fromFormat}`); 36 | } 37 | } else { 38 | fromFormat = exports.detectFormat(buffer); 39 | } 40 | 41 | if (fromFormat === toFormat) { 42 | return buffer; 43 | } 44 | if (fromFormat === 'woff') { 45 | buffer = woffTool.toSfnt(buffer); 46 | } else if (fromFormat === 'woff2') { 47 | buffer = Buffer.from(await wawoff2.decompress(buffer)); 48 | } 49 | 50 | if (toFormat === 'woff') { 51 | buffer = woffTool.toWoff(buffer); 52 | } else if (toFormat === 'woff2') { 53 | buffer = Buffer.from(await wawoff2.compress(buffer)); 54 | } 55 | return buffer; 56 | }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fontverter", 3 | "version": "2.0.0", 4 | "description": "Convert font buffers between TTF/WOFF/WOFF2", 5 | "main": "index.js", 6 | "dependencies": { 7 | "wawoff2": "^2.0.0", 8 | "woff2sfnt-sfnt2woff": "^1.0.0" 9 | }, 10 | "devDependencies": { 11 | "coveralls": "^3.0.2", 12 | "eslint": "^7.0.0", 13 | "eslint-config-prettier": "^7.0.0", 14 | "eslint-config-standard": "^16.0.0", 15 | "eslint-plugin-import": "^2.17.3", 16 | "eslint-plugin-mocha": "^8.0.0", 17 | "eslint-plugin-node": "^11.0.0", 18 | "eslint-plugin-promise": "^4.0.1", 19 | "eslint-plugin-standard": "^5.0.0", 20 | "mocha": "^7.0.0", 21 | "nyc": "^15.0.0", 22 | "offline-github-changelog": "^2.0.0", 23 | "prettier": "~2.2.0", 24 | "unexpected": "^12.0.0" 25 | }, 26 | "scripts": { 27 | "lint": "eslint . && prettier --check '**/*.{js,json,md}'", 28 | "test": "mocha", 29 | "test:ci": "npm run coverage", 30 | "coverage": "nyc --reporter=lcov --reporter=text --all -- npm test && echo google-chrome coverage/lcov-report/index.html", 31 | "preversion": "offline-github-changelog --next=${npm_package_version} > CHANGELOG.md && git add CHANGELOG.md" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/papandreou/fontverter.git" 36 | }, 37 | "keywords": [ 38 | "font", 39 | "convert", 40 | "transcode", 41 | "TTF", 42 | "WOFF", 43 | "WOFF2", 44 | "wasm" 45 | ], 46 | "author": "Andreas Lind ", 47 | "license": "BSD-3-Clause", 48 | "bugs": { 49 | "url": "https://github.com/papandreou/fontverter/issues" 50 | }, 51 | "homepage": "https://github.com/papandreou/fontverter#readme" 52 | } 53 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const fontverter = require('..'); 2 | const { readFile } = require('fs').promises; 3 | const pathModule = require('path'); 4 | const expect = require('unexpected').clone(); 5 | 6 | describe('fontverter.convert', function () { 7 | before(async function () { 8 | this.sfnt = await readFile( 9 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.ttf') 10 | ); 11 | this.woff = await readFile( 12 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.woff') 13 | ); 14 | this.woff2 = await readFile( 15 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.woff2') 16 | ); 17 | }); 18 | 19 | describe('when the source format is not given', function () { 20 | it('should throw if the source format could not be detected', async function () { 21 | expect( 22 | () => fontverter.convert(Buffer.from('abcd'), 'sfnt'), 23 | 'to error', 24 | 'Unrecognized font signature: abcd' 25 | ); 26 | }); 27 | 28 | it('should throw if the target format is not supported', async function () { 29 | expect( 30 | () => fontverter.convert(this.sfnt, 'footype'), 31 | 'to error', 32 | 'Unsupported target format: footype' 33 | ); 34 | }); 35 | 36 | it('should convert a sfnt font to sfnt', async function () { 37 | const buffer = await fontverter.convert(this.sfnt, 'sfnt'); 38 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 39 | expect(buffer, 'to be', this.sfnt); // Should be a noop 40 | }); 41 | 42 | it('should convert a sfnt font to woff', async function () { 43 | const buffer = await fontverter.convert(this.sfnt, 'woff'); 44 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 45 | }); 46 | 47 | it('should convert a sfnt font to woff2', async function () { 48 | const buffer = await fontverter.convert(this.sfnt, 'woff2'); 49 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 50 | }); 51 | 52 | it('should convert a woff font to sfnt', async function () { 53 | const buffer = await fontverter.convert(this.woff, 'sfnt'); 54 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 55 | }); 56 | 57 | it('should convert a woff font to woff', async function () { 58 | const buffer = await fontverter.convert(this.woff, 'woff'); 59 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 60 | expect(buffer, 'to be', this.woff); // Should be a noop 61 | }); 62 | 63 | it('should convert a woff font to woff2', async function () { 64 | const buffer = await fontverter.convert(this.woff, 'woff2'); 65 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 66 | }); 67 | 68 | it('should convert a woff2 font to sfnt', async function () { 69 | const buffer = await fontverter.convert(this.woff2, 'sfnt'); 70 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 71 | }); 72 | 73 | it('should convert a woff2 font to woff', async function () { 74 | const buffer = await fontverter.convert(this.woff2, 'woff'); 75 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 76 | }); 77 | 78 | it('should convert a woff2 font to woff2', async function () { 79 | const buffer = await fontverter.convert(this.woff2, 'woff2'); 80 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 81 | expect(buffer, 'to be', this.woff2); // Should be a noop 82 | }); 83 | }); 84 | 85 | describe('when the source format is given', function () { 86 | it('should throw if the source format is not supported', async function () { 87 | expect( 88 | () => fontverter.convert(Buffer.from('abcd'), 'sfnt', 'footype'), 89 | 'to error', 90 | 'Unsupported source format: footype' 91 | ); 92 | }); 93 | 94 | it('should convert a sfnt font to sfnt', async function () { 95 | const buffer = await fontverter.convert(this.sfnt, 'sfnt', 'sfnt'); 96 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 97 | expect(buffer, 'to be', this.sfnt); // Should be a noop 98 | }); 99 | 100 | it('should convert a sfnt font to sfnt, using truetype as an alias', async function () { 101 | const buffer = await fontverter.convert( 102 | this.sfnt, 103 | 'truetype', 104 | 'truetype' 105 | ); 106 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 107 | expect(buffer, 'to be', this.sfnt); // Should be a noop 108 | }); 109 | 110 | it('should convert a sfnt font to woff', async function () { 111 | const buffer = await fontverter.convert(this.sfnt, 'woff', 'sfnt'); 112 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 113 | }); 114 | 115 | it('should convert a sfnt font to woff2', async function () { 116 | const buffer = await fontverter.convert(this.sfnt, 'woff2', 'sfnt'); 117 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 118 | }); 119 | 120 | it('should convert a woff font to sfnt', async function () { 121 | const buffer = await fontverter.convert(this.woff, 'sfnt', 'woff'); 122 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 123 | }); 124 | 125 | it('should convert a woff font to woff', async function () { 126 | const buffer = await fontverter.convert(this.woff, 'woff', 'woff'); 127 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 128 | expect(buffer, 'to be', this.woff); // Should be a noop 129 | }); 130 | 131 | it('should convert a woff font to woff2', async function () { 132 | const buffer = await fontverter.convert(this.woff, 'woff2', 'woff'); 133 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 134 | }); 135 | 136 | it('should convert a woff2 font to sfnt', async function () { 137 | const buffer = await fontverter.convert(this.woff2, 'sfnt', 'woff2'); 138 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 139 | }); 140 | 141 | it('should convert a woff2 font to woff', async function () { 142 | const buffer = await fontverter.convert(this.woff2, 'woff', 'woff2'); 143 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 144 | }); 145 | 146 | it('should convert a woff2 font to woff2', async function () { 147 | const buffer = await fontverter.convert(this.woff2, 'woff2', 'woff2'); 148 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 149 | expect(buffer, 'to be', this.woff2); // Should be a noop 150 | }); 151 | }); 152 | }); 153 | 154 | describe('fontverter.detectFormat', function () { 155 | it('should throw if the contents of the buffer could not be recognized', async function () { 156 | expect( 157 | () => fontverter.convert(Buffer.from('abcd'), 'sfnt'), 158 | 'to error', 159 | 'Unrecognized font signature: abcd' 160 | ); 161 | }); 162 | 163 | it('should detect a sfnt font', async function () { 164 | const buffer = await readFile( 165 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.ttf') 166 | ); 167 | expect(fontverter.detectFormat(buffer), 'to equal', 'sfnt'); 168 | }); 169 | 170 | it('should detect a woff font', async function () { 171 | const buffer = await readFile( 172 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.woff') 173 | ); 174 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff'); 175 | }); 176 | 177 | it('should detect a woff2 font', async function () { 178 | const buffer = await readFile( 179 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.woff2') 180 | ); 181 | expect(fontverter.detectFormat(buffer), 'to equal', 'woff2'); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /testdata/Roboto-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/fontverter/b5615c91cd9f580bca744636f7fd89575b86a57e/testdata/Roboto-400.ttf -------------------------------------------------------------------------------- /testdata/Roboto-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/fontverter/b5615c91cd9f580bca744636f7fd89575b86a57e/testdata/Roboto-400.woff -------------------------------------------------------------------------------- /testdata/Roboto-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/fontverter/b5615c91cd9f580bca744636f7fd89575b86a57e/testdata/Roboto-400.woff2 --------------------------------------------------------------------------------