├── .eslintrc.yml ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin ├── utils.js ├── woff2_compress.js └── woff2_decompress.js ├── build ├── compress_binding.js └── decompress_binding.js ├── compress.js ├── decompress.js ├── index.js ├── package.json ├── src ├── Dockerfile ├── Makefile ├── README.md ├── compress_binding.cc └── decompress_binding.cc └── test ├── .eslintrc.yml ├── bin-utils.js ├── fixtures ├── sample.ttf ├── sample_compressed.woff2 └── sample_decompressed.ttf └── test.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | ignorePatterns: 2 | - build 3 | 4 | rules: 5 | camelcase: 0 6 | key-spacing: 0 7 | no-multi-spaces: 0 8 | no-multiple-empty-lines: 0 9 | padded-blocks: 0 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: puzrin 2 | patreon: puzrin 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * 3' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | 17 | - run: npm install 18 | - run: npm test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.bc 3 | 4 | /node_modules 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2.0.1 / 2022-01-19 2 | ------------------ 3 | 4 | - Changed build options to keep node.js `uncaughtException` & `unhandledRejection` 5 | handlers intact (#9). 6 | 7 | 8 | 2.0.0 / 2021-04-04 9 | ------------------ 10 | 11 | - Refactor build process to use docker. 12 | - Rewrite bindings with `EMSCRIPTEN_BINDINGS`. 13 | - Deps bump. 14 | 15 | 16 | 1.0.2 / 2018-03-22 17 | ------------------ 18 | 19 | - Improve CLI params support: single param & stdout. 20 | 21 | 22 | 1.0.1 / 2018-02-24 23 | ------------------ 24 | 25 | - Fix crash on big font files (when memory growth happens). 26 | - Declare symlinks for CLI scripts. 27 | 28 | 29 | 1.0.0 / 2018-02-15 30 | ------------------ 31 | 32 | - First release. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2017 by the WOFF2 Authors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | woff2 for node.js (via WebAssembly) 2 | =================================== 3 | 4 | [![CI](https://github.com/fontello/wawoff2/actions/workflows/ci.yml/badge.svg)](https://github.com/fontello/wawoff2/actions/workflows/ci.yml) 5 | [![NPM version](https://img.shields.io/npm/v/wawoff2.svg?style=flat)](https://www.npmjs.org/package/wawoff2) 6 | 7 | Google's [woff2](https://github.com/google/woff2) build for `node.js`, using 8 | WebAssembly. Why this is better than binary bindings: 9 | 10 | - works everywhere without rebuild 11 | 12 | 13 | Install 14 | ------- 15 | 16 | ```sh 17 | npm install wawoff2 18 | ``` 19 | 20 | 21 | Use Example 22 | ----------- 23 | 24 | ```js 25 | const wawoff = require('wawoff2'); 26 | 27 | // src - Buffer or Uint8Array 28 | wawoff.compress(src).then(out => { 29 | // store result 30 | }); 31 | ``` 32 | 33 | Command-line Example 34 | -------------------- 35 | 36 | To compress a `.ttf` file into a `.woff2` file: 37 | 38 | ```bash 39 | woff2_compress.js [-h] [-v] infile [outfile] 40 | 41 | Positional arguments: 42 | infile Input .ttf file 43 | outfile Output .woff2 file (- for stdout) 44 | 45 | Optional arguments: 46 | -h, --help Show this help message and exit. 47 | -v, --version Show program's version number and exit. 48 | ``` 49 | 50 | And the opposite, to decompress a `.woff2` file into a `.ttf` one: 51 | 52 | ```bash 53 | woff2_decompress.js [-h] [-v] infile [outfile] 54 | 55 | Positional arguments: 56 | infile Input .woff2 file 57 | outfile Output .ttf file (- for stdout) 58 | 59 | Optional arguments: 60 | -h, --help Show this help message and exit. 61 | -v, --version Show program's version number and exit. 62 | ``` 63 | -------------------------------------------------------------------------------- /bin/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { extname } = require('path') 4 | 5 | exports.swap_ext = (filename, from_ext, to_ext) => { 6 | const ext = extname(filename) 7 | 8 | // if filename has an extension, swap it 9 | if (ext === from_ext) { 10 | return filename.replace(new RegExp(from_ext + '$', 'i'), to_ext) 11 | } 12 | 13 | // otherwise force suffix 14 | return filename + to_ext 15 | } 16 | -------------------------------------------------------------------------------- /bin/woff2_compress.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | /* eslint-disable no-console */ 4 | 5 | 'use strict' 6 | 7 | const fs = require('fs') 8 | const argparse = require('argparse') 9 | 10 | const compress = require('../compress') 11 | const { swap_ext } = require('./utils') 12 | 13 | 14 | const parser = new argparse.ArgumentParser({ 15 | prog: 'woff2_compress.js', 16 | add_help: true 17 | }) 18 | 19 | parser.add_argument('-v', '--version', { 20 | action: 'version', 21 | version: require('../package.json').version 22 | }) 23 | 24 | parser.add_argument('infile', { 25 | nargs: 1, 26 | help: 'Input .ttf file' 27 | }) 28 | 29 | parser.add_argument('outfile', { 30 | nargs: '?', 31 | help: 'Output .woff2 file (- for stdout)' 32 | }) 33 | 34 | 35 | const args = parser.parse_args() 36 | const infile = args.infile[0] 37 | let outfile = args.outfile 38 | let input 39 | 40 | try { 41 | input = fs.readFileSync(infile) 42 | } catch (e) { 43 | console.error(`Can't open input file (${infile})`) 44 | process.exit(1) 45 | } 46 | 47 | compress(input).then(woff2 => { 48 | if (outfile === '-') { 49 | // convert UInt8Array into a disk writeable buffer 50 | process.stdout.write(Buffer.from(woff2)) 51 | } else { 52 | if (!outfile) { 53 | outfile = swap_ext(infile, '.ttf', '.woff2') 54 | } 55 | 56 | fs.writeFileSync(outfile, woff2) 57 | } 58 | }, error => { 59 | console.log(error) 60 | }) 61 | -------------------------------------------------------------------------------- /bin/woff2_decompress.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | /* eslint-disable no-console */ 4 | 5 | 'use strict' 6 | 7 | const fs = require('fs') 8 | const argparse = require('argparse') 9 | 10 | const decompress = require('../decompress') 11 | const { swap_ext } = require('./utils') 12 | 13 | 14 | const parser = new argparse.ArgumentParser({ 15 | prog: 'woff2_decompress.js', 16 | add_help: true 17 | }) 18 | 19 | parser.add_argument('-v', '--version', { 20 | action: 'version', 21 | version: require('../package.json').version 22 | }) 23 | 24 | parser.add_argument('infile', { 25 | nargs: 1, 26 | help: 'Input .woff2 file' 27 | }) 28 | 29 | parser.add_argument('outfile', { 30 | nargs: '?', 31 | help: 'Output .ttf file (- for stdout)' 32 | }) 33 | 34 | 35 | const args = parser.parse_args() 36 | const infile = args.infile[0] 37 | let outfile = args.outfile 38 | let input 39 | 40 | try { 41 | input = fs.readFileSync(infile) 42 | } catch (e) { 43 | console.error(`Can't open input file (${infile})`) 44 | process.exit(1) 45 | } 46 | 47 | decompress(input).then(ttf => { 48 | if (outfile === '-') { 49 | // convert UInt8Array into a disk writeable buffer 50 | process.stdout.write(Buffer.from(ttf)) 51 | } else { 52 | if (!outfile) { 53 | outfile = swap_ext(infile, '.woff2', '.ttf') 54 | } 55 | 56 | fs.writeFileSync(outfile, ttf) 57 | } 58 | }, error => { 59 | console.log(error) 60 | }) 61 | -------------------------------------------------------------------------------- /compress.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const em_module = require('./build/compress_binding.js') 4 | 5 | const runtimeInit = new Promise(resolve => { 6 | em_module.onRuntimeInitialized = resolve 7 | }) 8 | 9 | module.exports = async function compress (buffer) { 10 | await runtimeInit 11 | const result = em_module.compress(buffer) 12 | if (result === false) throw new Error('ConvertTTFToWOFF2 failed') 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /decompress.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const em_module = require('./build/decompress_binding.js') 4 | 5 | const runtimeInit = new Promise(resolve => { 6 | em_module.onRuntimeInitialized = resolve 7 | }) 8 | 9 | module.exports = async function decompress (buffer) { 10 | await runtimeInit 11 | const result = em_module.decompress(buffer) 12 | if (result === false) throw new Error('ConvertWOFF2ToTTF failed') 13 | return result 14 | } 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.compress = require('./compress') 4 | exports.decompress = require('./decompress') 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wawoff2", 3 | "version": "2.0.1", 4 | "description": "Convert TTF font to WOFF2", 5 | "keywords": [ 6 | "font", 7 | "ttf", 8 | "woff", 9 | "woff2", 10 | "convertor" 11 | ], 12 | "license": "MIT", 13 | "repository": "fontello/wawoff2", 14 | "scripts": { 15 | "build": "docker build -t wawoff2_build ./src && docker run --rm -v $(pwd):/src/wawoff2 -u $(id -u):$(id -g) -it wawoff2_build make -C /src/wawoff2/src", 16 | "test": "standardx -v . && mocha ./test" 17 | }, 18 | "bin": { 19 | "woff2_compress.js": "./bin/woff2_compress.js", 20 | "woff2_decompress.js": "./bin/woff2_decompress.js" 21 | }, 22 | "files": [ 23 | "compress.js", 24 | "decompress.js", 25 | "index.js", 26 | "bin/", 27 | "build/" 28 | ], 29 | "dependencies": { 30 | "argparse": "^2.0.1" 31 | }, 32 | "devDependencies": { 33 | "mocha": "^9.1.4", 34 | "standardx": "^7.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM emscripten/emsdk:2.0.34 2 | 3 | RUN git clone --recursive https://github.com/google/woff2.git 4 | RUN cd woff2 && \ 5 | git checkout a0d0ed7da27b708c0a4e96ad7a998bddc933c06e 6 | 7 | RUN mkdir -p /src/build/brotli-wasm && \ 8 | cd /src/build/brotli-wasm && \ 9 | emcmake cmake /src/woff2/brotli -DCMAKE_BUILD_TYPE=Release && \ 10 | emmake make -j4 11 | 12 | RUN mkdir -p /src/build/brotli-native && \ 13 | cd /src/build/brotli-native && \ 14 | cmake /src/woff2/brotli -DCMAKE_BUILD_TYPE=Release && \ 15 | make -j4 16 | 17 | RUN mkdir -p /src/build/woff2-wasm && \ 18 | cd /src/build/woff2-wasm && \ 19 | emcmake cmake /src/woff2 \ 20 | -DCMAKE_BUILD_TYPE=Release \ 21 | -DNOISY_LOGGING=OFF \ 22 | -DBROTLIENC_INCLUDE_DIRS=/src/woff2/brotli/c/include/ \ 23 | -DBROTLIDEC_INCLUDE_DIRS=/src/woff2/brotli/c/include/ \ 24 | -DBROTLIENC_LIBRARIES=/src/build/brotli-wasm/libbrotlienc.a \ 25 | -DBROTLIDEC_LIBRARIES=/src/build/brotli-wasm/libbrotlidec.a \ 26 | && \ 27 | emmake make -j4 woff2enc woff2dec 28 | 29 | RUN mkdir -p /src/build/woff2-native && \ 30 | cd /src/build/woff2-native && \ 31 | cmake /src/woff2 \ 32 | -DCMAKE_BUILD_TYPE=Release \ 33 | -DBROTLIDEC_INCLUDE_DIRS=/src/woff2/brotli/c/include/ \ 34 | -DBROTLIENC_INCLUDE_DIRS=/src/woff2/brotli/c/include/ \ 35 | -DBROTLIDEC_LIBRARIES=/src/build/brotli-native/libbrotlidec.so \ 36 | -DBROTLIENC_LIBRARIES=/src/build/brotli-native/libbrotlienc.so \ 37 | && \ 38 | make -j4 woff2_compress woff2_decompress 39 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | CARGS=--bind -s NODEJS_CATCH_REJECTION=0 -s NODEJS_CATCH_EXIT=0 -s ALLOW_MEMORY_GROWTH=1 -s SINGLE_FILE=1 -O3 2 | FIXTURES=/src/wawoff2/test/fixtures 3 | 4 | all: 5 | mkdir -p /src/wawoff2/build 6 | 7 | emcc $(CARGS) -I/src/woff2/include/ \ 8 | -o /src/wawoff2/build/compress_binding.js \ 9 | /src/wawoff2/src/compress_binding.cc \ 10 | /src/build/woff2-wasm/libwoff2enc.a \ 11 | /src/build/woff2-wasm/libwoff2common.a \ 12 | /src/build/brotli-wasm/libbrotlienc.a \ 13 | /src/build/brotli-wasm/libbrotlicommon.a 14 | 15 | emcc $(CARGS) -I/src/woff2/include/ \ 16 | -o /src/wawoff2/build/decompress_binding.js \ 17 | /src/wawoff2/src/decompress_binding.cc \ 18 | /src/build/woff2-wasm/libwoff2dec.a \ 19 | /src/build/woff2-wasm/libwoff2common.a \ 20 | /src/build/brotli-wasm/libbrotlidec.a \ 21 | /src/build/brotli-wasm/libbrotlicommon.a 22 | 23 | /src/build/woff2-native/woff2_compress $(FIXTURES)/sample.ttf 24 | mv $(FIXTURES)/sample.woff2 $(FIXTURES)/sample_compressed.woff2 25 | 26 | /src/build/woff2-native/woff2_decompress $(FIXTURES)/sample_compressed.woff2 27 | mv $(FIXTURES)/sample_compressed.ttf $(FIXTURES)/sample_decompressed.ttf 28 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | Use `npm run build` to rebuild sources. -------------------------------------------------------------------------------- /src/compress_binding.cc: -------------------------------------------------------------------------------- 1 | /* 2 | Distributed under MIT license. 3 | See file LICENSE for detail or copy at https://opensource.org/licenses/MIT 4 | */ 5 | 6 | #include 7 | #include 8 | 9 | 10 | emscripten::val compress(std::string input) { 11 | const uint8_t* input_data = reinterpret_cast(input.data()); 12 | size_t output_size = woff2::MaxWOFF2CompressedSize(input_data, input.size()); 13 | std::string output(output_size, 0); 14 | uint8_t* output_data = reinterpret_cast(&output[0]); 15 | 16 | woff2::WOFF2Params params; 17 | if (!woff2::ConvertTTFToWOFF2(input_data, input.size(), 18 | output_data, &output_size, params)) { 19 | return emscripten::val(false); 20 | } 21 | output.resize(output_size); 22 | 23 | return emscripten::val( 24 | emscripten::typed_memory_view(output.size(), reinterpret_cast(output.data())) 25 | ); 26 | } 27 | 28 | 29 | EMSCRIPTEN_BINDINGS(wawoff2) { 30 | emscripten::function("compress", &compress); 31 | } 32 | -------------------------------------------------------------------------------- /src/decompress_binding.cc: -------------------------------------------------------------------------------- 1 | /* 2 | Distributed under MIT license. 3 | See file LICENSE for detail or copy at https://opensource.org/licenses/MIT 4 | */ 5 | 6 | #include 7 | #include 8 | 9 | 10 | emscripten::val decompress(std::string input) { 11 | const uint8_t* raw_input = reinterpret_cast(input.data()); 12 | 13 | std::string output( 14 | std::min(woff2::ComputeWOFF2FinalSize(raw_input, input.size()), woff2::kDefaultMaxSize), 15 | 0); 16 | 17 | woff2::WOFF2StringOut out(&output); 18 | 19 | if (!woff2::ConvertWOFF2ToTTF(raw_input, input.size(), &out)) { 20 | return emscripten::val(false); 21 | } 22 | 23 | return emscripten::val( 24 | emscripten::typed_memory_view(output.size(), reinterpret_cast(output.data())) 25 | ); 26 | } 27 | 28 | 29 | EMSCRIPTEN_BINDINGS(wawoff2) { 30 | emscripten::function("decompress", &decompress); 31 | } 32 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/bin-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | 5 | const utils = require('../bin/utils') 6 | 7 | describe('swap_ext', function () { 8 | const { swap_ext } = utils 9 | 10 | it('swaps .ttf suffix with .woff2', () => { 11 | assert.equal(swap_ext('font.ttf', '.ttf', '.woff2'), 'font.woff2') 12 | }) 13 | 14 | it('suffixes with .woff2', () => { 15 | assert.equal(swap_ext('font', '.ttf', '.woff2'), 'font.woff2') 16 | }) 17 | 18 | it('suffixes with .woff2 anyway', () => { 19 | assert.equal(swap_ext('font.otf', '.ttf', '.woff2'), 'font.otf.woff2') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/fixtures/sample.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fontello/wawoff2/2ba75100c9ec6031a5a1d32ddd1120409aa68573/test/fixtures/sample.ttf -------------------------------------------------------------------------------- /test/fixtures/sample_compressed.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fontello/wawoff2/2ba75100c9ec6031a5a1d32ddd1120409aa68573/test/fixtures/sample_compressed.woff2 -------------------------------------------------------------------------------- /test/fixtures/sample_decompressed.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fontello/wawoff2/2ba75100c9ec6031a5a1d32ddd1120409aa68573/test/fixtures/sample_decompressed.ttf -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const assert = require('assert') 4 | const read = require('fs').readFileSync 5 | const join = require('path').join 6 | 7 | const wawoff2 = require('../') 8 | 9 | 10 | describe('chain', function () { 11 | 12 | const sample = Uint8Array.from(read(join(__dirname, './fixtures/sample.ttf'))) 13 | const sample_compressed = Uint8Array.from(read(join(__dirname, './fixtures/sample_compressed.woff2'))) 14 | const sample_decompressed = Uint8Array.from(read(join(__dirname, './fixtures/sample_decompressed.ttf'))) 15 | 16 | it('compress', async function () { 17 | this.timeout(3000) 18 | 19 | const out = await wawoff2.compress(sample) 20 | assert.deepEqual(out, sample_compressed) 21 | }) 22 | 23 | it('decompress', async function () { 24 | const out = await wawoff2.decompress(sample_compressed) 25 | assert.deepEqual(out, sample_decompressed) 26 | }) 27 | 28 | }) 29 | --------------------------------------------------------------------------------