├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── test └── index.js └── testdata ├── FZZJ-ZSXKJW.ttf ├── MaterialIcons-Regular.ttf ├── OpenSans.ttf ├── PadaukBook-Regular.ttf ├── Roboto-400.woff2 ├── RobotoFlex-VariableFont_GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf ├── SourceHanSerifCN-SemiBold.otf ├── emoji.ttf └── k3k702ZOKiLJc3WVjuplzHhCUOGz7vYGh680lGh-uXM.woff /.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-22.04 9 | name: Node ${{ matrix.node }} 10 | strategy: 11 | matrix: 12 | node: 13 | - '14' 14 | - '16' 15 | - '18' 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-22.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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Test suite", 11 | "program": "${workspaceFolder}/node_modules/.bin/_mocha", 12 | "args": ["--timeout", "0"], 13 | "skipFiles": [ 14 | "/**" // Prevent stepping through async_hooks.js et al. 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v2.4.0 (2024-10-05) 2 | 3 | - [Update harfbuzzjs to ^0.4.0](https://github.com/papandreou/subset-font/commit/c3b626ce3b2f9f83d8c3b2c618b3cc779bf28af2) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 4 | - [Update harfbuzzjs to ^0.3.6](https://github.com/papandreou/subset-font/commit/3f711c8aa29a426c7f22655861abfb976950f527) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 5 | - [Rename for clarity: exports => harfbuzzJsWasm](https://github.com/papandreou/subset-font/commit/62e2eb51a6509a37bf5cc326280244d324208aeb) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 6 | 7 | ### v2.3.0 (2024-03-25) 8 | 9 | - [Implement noLayoutClosure flag, closes \#22](https://github.com/papandreou/subset-font/commit/154077d5208029ceec9ee2258ab7f4ea40d0c0d9) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 10 | 11 | ### v2.2.0 (2024-03-24) 12 | 13 | #### Pull requests 14 | 15 | - [#23](https://github.com/papandreou/subset-font/pull/23) Support reducing the variation space of individual axes \(partial instancing\) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com), [Andreas Lind](mailto:andreaslindpetersen@gmail.com), [Andreas Lind](mailto:andreaslindpetersen@gmail.com), [Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 16 | 17 | #### Commits to master 18 | 19 | - [Update harfbuzzjs to ^0.3.4](https://github.com/papandreou/subset-font/commit/8118084717dea57bd2f01b99a49ed5554ac52bad) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 20 | - [Exclude test and testdata from the npm package](https://github.com/papandreou/subset-font/commit/6fde19fb442a62081054a99e68452721f1678f4b) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 21 | 22 | ### v2.1.0 (2023-04-06) 23 | 24 | #### Pull requests 25 | 26 | - [#21](https://github.com/papandreou/subset-font/pull/21) Add support for instancing variable fonts \(pinning variation axes\) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 27 | 28 | #### Commits to master 29 | 30 | - [Update harfbuzzjs to ^0.3.2](https://github.com/papandreou/subset-font/commit/9fb043c2bba16ba26978c4c42a980c26a2f3f428) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 31 | 32 | ### v2.0.0 (2022-12-18) 33 | 34 | - [Upgrade to Ubuntu 22.04](https://github.com/papandreou/subset-font/commit/0714132362ad499979dae4571aa03884d513cec1) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 35 | - [Revert "Revert "Update harfbuzzjs to ^0.3.1""](https://github.com/papandreou/subset-font/commit/b526500cdbfbbfaab6bb68092b3db0fdd6f00d97) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 36 | - [Drop node.js 10 and 12 support, add 16 and 18](https://github.com/papandreou/subset-font/commit/b276f036429681193b6b2b8f79478a9b7fab40ff) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 37 | 38 | ### v1.7.1 (2022-12-18) 39 | 40 | - [Revert "Update harfbuzzjs to ^0.3.1"](https://github.com/papandreou/subset-font/commit/c3c5cb98fb11b137d3d8162128dd3a1789ecfca4) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 41 | 42 | ### v1.7.0 (2022-12-18) 43 | 44 | - [Update harfbuzzjs to ^0.3.1](https://github.com/papandreou/subset-font/commit/27a4863063e633d4bbc3d3339b5fc0cfcb61927b) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 45 | - [Test that we strip tables not required by web browsers](https://github.com/papandreou/subset-font/commit/e90889e7e5d3f3373c076399a523e87fd4c2e8c2) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 46 | 47 | ### v1.6.1 (2022-07-30) 48 | 49 | - [Revert "Don't limit to a concurrency of 1 now that harfbuzzjs auto-grows the wasm heap"](https://github.com/papandreou/subset-font/commit/b5461276536239cf865122dae67b1fbdf067e1ae) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 50 | 51 | ### v1.6.0 (2022-07-30) 52 | 53 | - [Don't limit to a concurrency of 1 now that harfbuzzjs auto-grows the wasm heap](https://github.com/papandreou/subset-font/commit/e09399546948a49cba9702fb29108b607b74bd7b) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 54 | 55 | ### v1.5.0 (2022-07-24) 56 | 57 | - [Update harfbuzzjs to ^0.3.0](https://github.com/papandreou/subset-font/commit/d38cb12bc204f63213c63a8e9217c64429379419) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 58 | - [Remove the memory.grow hack now that harfbuzzjs uses Emscripten and automatically grows the heap](https://github.com/papandreou/subset-font/commit/0bf5d7ab7ab2df35e863a944ebf1d87f91f777b3) ([Andreas Lind](mailto:andreas.lind@workday.com)) 59 | - [hb-subset.wasm moved to the root of the harfbuzzjs package](https://github.com/papandreou/subset-font/commit/72ff88a4479a3a23d0d0a29d3102c2fbb2aa0b7f) ([Andreas Lind](mailto:andreas.lind@workday.com)) 60 | - [Add a regression test with a huge font from Munter\/subfont\#145](https://github.com/papandreou/subset-font/commit/8a4667271239f415f84ef48633e6ca13d3456eb2) ([Andreas Lind](mailto:andreas.lind@workday.com)) 61 | - [Fix CHANGELOG generation in preversion script now that an npm env var changed](https://github.com/papandreou/subset-font/commit/66a7ae5586a3a26380805297abc31b1176a9bb9c) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 62 | - [+1 more](https://github.com/papandreou/subset-font/compare/v1.4.0...v1.5.0) 63 | 64 | ### v1.4.0 (2021-11-10) 65 | 66 | - [#13](https://github.com/papandreou/subset-font/pull/13) Update harfbuzzjs to ^0.2.0 \(harfbuzz 3.0.0\) ([Andreas Lind](mailto:andreas.lind@workday.com), [Andreas Lind](mailto:andreas.lind@workday.com), [Andreas Lind](mailto:andreas.lind@workday.com), [Andreas Lind](mailto:andreas.lind@workday.com), [Andreas Lind](mailto:andreaslindpetersen@gmail.com), [Andreas Lind](mailto:andreaslindpetersen@gmail.com), [Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 67 | 68 | ### v1.3.3 (2021-07-04) 69 | 70 | - [#10](https://github.com/papandreou/subset-font/pull/10) declare lodash as a prod dep ([alsotang](mailto:alsotang@gmail.com)) 71 | 72 | ### v1.3.2 (2021-07-02) 73 | 74 | #### Pull requests 75 | 76 | - [#9](https://github.com/papandreou/subset-font/pull/9) destroy hb\_face correctly ([alsotang](mailto:alsotang@gmail.com)) 77 | 78 | #### Commits to master 79 | 80 | - [Truncate the now complete FZZJ-ZSXKJW.ttf in the test](https://github.com/papandreou/subset-font/commit/1b9d00675ad2d3001b99512e7193fd012284363e) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 81 | 82 | ### v1.3.1 (2021-07-02) 83 | 84 | - [Add vscode debugger launch config](https://github.com/papandreou/subset-font/commit/12b89ce1226a8622adca1acd4c29d8260f0ab8e2) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 85 | - [Add a small bit of error handling, copied from hb-subset](https://github.com/papandreou/subset-font/commit/72b5b99c2190d9b81d5eb99a69f086e3a436d9b0) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 86 | - [Error out if the text isn't given as a string](https://github.com/papandreou/subset-font/commit/f4a5297780289e69a695cbd3158eff667ec7b971) ([Andreas Lind](mailto:andreaslindpetersen@gmail.com)) 87 | 88 | ### v1.3.0 (2021-07-01) 89 | 90 | - [Mention the preserveNameIds option in the README](https://github.com/papandreou/subset-font/commit/ef2a8b2fddcc4f1245119a6eca010d3436375e4f) ([Andreas Lind](mailto:andreas.lind@workday.com)) 91 | - [Use fontkit to look for the presence of a licenseURL in the tests, avoiding ttx](https://github.com/papandreou/subset-font/commit/4e97447a86d6b0f52cd510e7fa4c34e5fea856ef) ([Andreas Lind](mailto:andreas.lind@workday.com)) 92 | - [Implement support for a preserveNameIds array](https://github.com/papandreou/subset-font/commit/00816d7821cd6bdaa01be909d93ec93c3f81fa36) ([Andreas Lind](mailto:andreas.lind@workday.com)) 93 | 94 | ### v1.2.3 (2021-05-17) 95 | 96 | - [Increase the WebAssembly heap size to accommodate larger fonts](https://github.com/papandreou/subset-font/commit/3dfc48a77264673668e34000877082819c37ce75) ([Andreas Lind](mailto:andreas.lind@workday.com)) 97 | 98 | ### v1.2.2 (2021-05-03) 99 | 100 | - [#5](https://github.com/papandreou/subset-font/pull/5) Update harfbuzzjs version and remove manual layout subset enable ([Ebrahim Byagowi](mailto:ebrahim@gnu.org)) 101 | 102 | ### v1.2.1 (2021-05-03) 103 | 104 | - [#4](https://github.com/papandreou/subset-font/pull/4) Simplify characters iteration ([ebraminio](mailto:ebrahim@gnu.org)) 105 | 106 | ### v1.2.0 (2021-04-20) 107 | 108 | - [Call it sfnt instead of truetype](https://github.com/papandreou/subset-font/commit/bb581a20f44617f2fa32a73c92a6f3aba438b4e4) ([Andreas Lind](mailto:andreas.lind@workday.com)) 109 | 110 | ### v1.1.3 (2021-04-15) 111 | 112 | #### Pull requests 113 | 114 | - [#2](https://github.com/papandreou/subset-font/pull/2) Fix a leak, handle non-BMP characters and performance tweaks ([Ebrahim Byagowi](mailto:ebrahim@gnu.org), [Ebrahim Byagowi](mailto:ebrahim@gnu.org), [Ebrahim Byagowi](mailto:ebrahim@gnu.org), [Ebrahim Byagowi](mailto:ebrahim@gnu.org)) 115 | 116 | #### Commits to master 117 | 118 | - [Use for...of without reintroducing the bug](https://github.com/papandreou/subset-font/commit/84ac1955987f5197b0f037d6cf0dde1622d73397) ([Andreas Lind](mailto:andreas.lind@workday.com)) 119 | 120 | ### v1.1.2 (2021-04-14) 121 | 122 | - [Free the original fontBuffer after subsetting](https://github.com/papandreou/subset-font/commit/1170630a1cb3be4a5279facc75cecfd5220ede1f) ([Andreas Lind](mailto:andreas.lind@workday.com)) 123 | 124 | ### v1.1.1 (2021-04-09) 125 | 126 | - [Update harfbuzzjs to ^0.1.4](https://github.com/papandreou/subset-font/commit/cafa582138a368129d674113b2be18000f9274e3) ([Andreas Lind](mailto:andreas.lind@workday.com)) 127 | - [Revert "Switch to a harfbuzzjs fork with a newer build"](https://github.com/papandreou/subset-font/commit/0f2509c908c7aa1e7d4b069bda336e5c08f13de6) ([Andreas Lind](mailto:andreas.lind@workday.com)) 128 | 129 | ### v1.1.0 (2021-04-09) 130 | 131 | - [Switch to a harfbuzzjs fork with a newer build](https://github.com/papandreou/subset-font/commit/78995cf5daf9c2dfdc5d14b3e919e1bd17b5d0e0) ([Andreas Lind](mailto:andreas.lind@workday.com)) 132 | - [More README](https://github.com/papandreou/subset-font/commit/32e03b8862452717b00487b899d0faf9b73e3138) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 133 | 134 | ### v1.0.0 (2021-02-06) 135 | 136 | - [Initial commit](https://github.com/papandreou/subset-font/commit/4b4d722bf9ac9604fd4a9002b7c7c2a0ff025d82) ([Andreas Lind](mailto:andreas.lind@peakon.com)) 137 | -------------------------------------------------------------------------------- /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 | # subset-font 2 | 3 | Create a subset font from an existing font in SFNT (TrueType/OpenType), WOFF, or WOFF2 format. When subsetting a variable font, you can also reduce the variation space at the individual axis level. 4 | 5 | These operations are implemented using [`harfbuzzjs`](https://github.com/harfbuzz/harfbuzzjs), which is a WebAssembly build of [HarfBuzz](https://harfbuzz.github.io/). 6 | 7 | ## Basic example 8 | 9 | ```js 10 | const subsetFont = require('subset-font'); 11 | 12 | const mySfntFontBuffer = Buffer.from(/*...*/); 13 | 14 | // Create a new font with only the characters required to render "Hello, world!" in WOFF2 format: 15 | const subsetBuffer = await subsetFont(mySfntFontBuffer, 'Hello, world!', { 16 | targetFormat: 'woff2', 17 | }); 18 | ``` 19 | 20 | ## Reducing the variation space 21 | 22 | ```js 23 | const subsetFont = require('subset-font'); 24 | 25 | const mySfntFontBuffer = Buffer.from(/*...*/); 26 | 27 | // Create a new font with only the characters required to render "Hello, world!" in WOFF2 format: 28 | const subsetBuffer = await subsetFont(mySfntFontBuffer, 'Hello, world!', { 29 | targetFormat: 'woff2', 30 | variationAxes: { 31 | // Pin the axis to 200: 32 | wght: 200, 33 | // Reduce the variation space, explicitly setting a new default value: 34 | GRAD: { min: -50, max: 50, default: 25 }, 35 | // Reduce the variation space. A new default value will be inferred by clamping the old default to the new range: 36 | slnt: { min: -9, max: 0 }, 37 | // The remaining axes will be kept as-is 38 | }, 39 | }); 40 | ``` 41 | 42 | ## API 43 | 44 | #### `subsetFont(buffer, text, options): Promise` 45 | 46 | Asynchronously create a subset font as a Buffer instance, optionally converting it to another format. 47 | 48 | Returns a promise that gets fulfilled with the subset font as a Buffer instance, or rejected with an error. 49 | 50 | Options: 51 | 52 | - `targetFormat` - the format to output, can be either `'sfnt'`, `'woff'`, or `'woff2'`. 53 | - `preserveNameIds` - an array of numbers specifying the extra name ids to preserve in the `name` table. By default the harfbuzz subsetter drops most of these. Use case described [here](https://github.com/papandreou/subset-font/issues/7). 54 | - `variationAxes` - an object specifying a full or partial instancing of variation axes in the font. Only works with variable fonts. See the example above. 55 | - `noLayoutClosure` - don't perform glyph closure for layout substitution (GSUB). Equivalent to `hb-subset --no-layout-closure` and `pyftsubset --no-layout-closure`. 56 | 57 | For backwards compatibility reasons, `'truetype'` is supported as an alias for `'sfnt'`. 58 | 59 | ## Why not use harfbuzzjs directly? 60 | 61 | This middle-man module only really exists for convenience. 62 | 63 | - `harfbuzzjs` is deliberately low-level bindings for HarfBuzz. While very flexible, it means that you need a series of hard-to-get-right incantations to move data in and out of the WebAssembly heap and carry out a subsetting operation. See [harfbuzz/harfbuzzjs#9](https://github.com/harfbuzz/harfbuzzjs/issues/9). 64 | - The subsetting routines in HarfBuzz only support the SFNT (TrueType/OpenType) format. `subset-font` adds support for reading and writing WOFF and WOFF2 via the [`fontverter`](https://github.com/papandreou/fontverter) library. 65 | 66 | ## Releases 67 | 68 | [Changelog](https://github.com/papandreou/subset-font/blob/master/CHANGELOG.md) 69 | 70 | ## License 71 | 72 | 3-clause BSD license -- see the `LICENSE` file for details. 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global WebAssembly */ 2 | const { readFile } = require('fs').promises; 3 | const _ = require('lodash'); 4 | const fontverter = require('fontverter'); 5 | 6 | const loadAndInitializeHarfbuzz = _.once(async () => { 7 | const { 8 | instance: { exports: harfbuzzJsWasm }, 9 | } = await WebAssembly.instantiate( 10 | await readFile(require.resolve('harfbuzzjs/hb-subset.wasm')) 11 | ); 12 | 13 | const heapu8 = new Uint8Array(harfbuzzJsWasm.memory.buffer); 14 | return { harfbuzzJsWasm, heapu8 }; 15 | }); 16 | 17 | function HB_TAG(str) { 18 | return str.split('').reduce(function (a, ch) { 19 | return (a << 8) + ch.charCodeAt(0); 20 | }, 0); 21 | } 22 | 23 | async function subsetFont( 24 | originalFont, 25 | text, 26 | { 27 | targetFormat = fontverter.detectFormat(originalFont), 28 | preserveNameIds, 29 | variationAxes, 30 | noLayoutClosure, 31 | } = {} 32 | ) { 33 | if (typeof text !== 'string') { 34 | throw new Error('The subset text must be given as a string'); 35 | } 36 | 37 | const { harfbuzzJsWasm, heapu8 } = await loadAndInitializeHarfbuzz(); 38 | 39 | originalFont = await fontverter.convert(originalFont, 'truetype'); 40 | 41 | const input = harfbuzzJsWasm.hb_subset_input_create_or_fail(); 42 | if (input === 0) { 43 | throw new Error( 44 | 'hb_subset_input_create_or_fail (harfbuzz) returned zero, indicating failure' 45 | ); 46 | } 47 | 48 | const fontBuffer = harfbuzzJsWasm.malloc(originalFont.byteLength); 49 | heapu8.set(new Uint8Array(originalFont), fontBuffer); 50 | 51 | // Create the face 52 | const blob = harfbuzzJsWasm.hb_blob_create( 53 | fontBuffer, 54 | originalFont.byteLength, 55 | 2, // HB_MEMORY_MODE_WRITABLE 56 | 0, 57 | 0 58 | ); 59 | const face = harfbuzzJsWasm.hb_face_create(blob, 0); 60 | harfbuzzJsWasm.hb_blob_destroy(blob); 61 | 62 | // Do the equivalent of --font-features=* 63 | const layoutFeatures = harfbuzzJsWasm.hb_subset_input_set( 64 | input, 65 | 6 // HB_SUBSET_SETS_LAYOUT_FEATURE_TAG 66 | ); 67 | harfbuzzJsWasm.hb_set_clear(layoutFeatures); 68 | harfbuzzJsWasm.hb_set_invert(layoutFeatures); 69 | 70 | if (preserveNameIds) { 71 | const inputNameIds = harfbuzzJsWasm.hb_subset_input_set( 72 | input, 73 | 4 // HB_SUBSET_SETS_NAME_ID 74 | ); 75 | for (const nameId of preserveNameIds) { 76 | harfbuzzJsWasm.hb_set_add(inputNameIds, nameId); 77 | } 78 | } 79 | 80 | if (noLayoutClosure) { 81 | harfbuzzJsWasm.hb_subset_input_set_flags( 82 | input, 83 | harfbuzzJsWasm.hb_subset_input_get_flags(input) | 0x00000200 // HB_SUBSET_FLAGS_NO_LAYOUT_CLOSURE 84 | ); 85 | } 86 | 87 | // Add unicodes indices 88 | const inputUnicodes = harfbuzzJsWasm.hb_subset_input_unicode_set(input); 89 | for (const c of text) { 90 | harfbuzzJsWasm.hb_set_add(inputUnicodes, c.codePointAt(0)); 91 | } 92 | 93 | if (variationAxes) { 94 | for (const [axisName, value] of Object.entries(variationAxes)) { 95 | if (typeof value === 'number') { 96 | // Simple case: Pin/instance the variation axis to a single value 97 | if ( 98 | !harfbuzzJsWasm.hb_subset_input_pin_axis_location( 99 | input, 100 | face, 101 | HB_TAG(axisName), 102 | value 103 | ) 104 | ) { 105 | harfbuzzJsWasm.hb_face_destroy(face); 106 | harfbuzzJsWasm.free(fontBuffer); 107 | throw new Error( 108 | `hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning ${axisName} to ${value}, indicating failure. Maybe the axis does not exist in the font?` 109 | ); 110 | } 111 | } else if (value && typeof value === 'object') { 112 | // Complex case: Reduce the variation space of the axis 113 | if ( 114 | typeof value.min === 'undefined' || 115 | typeof value.max === 'undefined' 116 | ) { 117 | harfbuzzJsWasm.hb_face_destroy(face); 118 | harfbuzzJsWasm.free(fontBuffer); 119 | throw new Error( 120 | `${axisName}: You must provide both a min and a max value when setting the axis range` 121 | ); 122 | } 123 | if ( 124 | !harfbuzzJsWasm.hb_subset_input_set_axis_range( 125 | input, 126 | face, 127 | HB_TAG(axisName), 128 | value.min, 129 | value.max, 130 | // An explicit NaN makes harfbuzz use the existing default value, clamping to the new range if necessary 131 | value.default ?? NaN 132 | ) 133 | ) { 134 | harfbuzzJsWasm.hb_face_destroy(face); 135 | harfbuzzJsWasm.free(fontBuffer); 136 | throw new Error( 137 | `hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of ${axisName} to [${value.min}; ${value.max}] and a default value of ${value.default}, indicating failure. Maybe the axis does not exist in the font?` 138 | ); 139 | } 140 | } 141 | } 142 | } 143 | 144 | let subset; 145 | try { 146 | subset = harfbuzzJsWasm.hb_subset_or_fail(face, input); 147 | if (subset === 0) { 148 | harfbuzzJsWasm.hb_face_destroy(face); 149 | harfbuzzJsWasm.free(fontBuffer); 150 | throw new Error( 151 | 'hb_subset_or_fail (harfbuzz) returned zero, indicating failure. Maybe the input file is corrupted?' 152 | ); 153 | } 154 | } finally { 155 | // Clean up 156 | harfbuzzJsWasm.hb_subset_input_destroy(input); 157 | } 158 | 159 | // Get result blob 160 | const result = harfbuzzJsWasm.hb_face_reference_blob(subset); 161 | 162 | const offset = harfbuzzJsWasm.hb_blob_get_data(result, 0); 163 | const subsetByteLength = harfbuzzJsWasm.hb_blob_get_length(result); 164 | if (subsetByteLength === 0) { 165 | harfbuzzJsWasm.hb_blob_destroy(result); 166 | harfbuzzJsWasm.hb_face_destroy(subset); 167 | harfbuzzJsWasm.hb_face_destroy(face); 168 | harfbuzzJsWasm.free(fontBuffer); 169 | throw new Error( 170 | 'Failed to create subset font, maybe the input file is corrupted?' 171 | ); 172 | } 173 | 174 | const subsetFont = Buffer.from( 175 | heapu8.subarray(offset, offset + subsetByteLength) 176 | ); 177 | 178 | // Clean up 179 | harfbuzzJsWasm.hb_blob_destroy(result); 180 | harfbuzzJsWasm.hb_face_destroy(subset); 181 | harfbuzzJsWasm.hb_face_destroy(face); 182 | harfbuzzJsWasm.free(fontBuffer); 183 | 184 | return await fontverter.convert(subsetFont, targetFormat, 'truetype'); 185 | } 186 | 187 | const limiter = require('p-limit')(1); 188 | module.exports = (...args) => limiter(() => subsetFont(...args)); 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subset-font", 3 | "version": "2.4.0", 4 | "description": "Create a subset of a TTF/WOFF/WOFF2 font using the wasm build of harfbuzz/hb-subset", 5 | "main": "index.js", 6 | "dependencies": { 7 | "fontverter": "^2.0.0", 8 | "harfbuzzjs": "^0.4.0", 9 | "lodash": "^4.17.21", 10 | "p-limit": "^3.1.0" 11 | }, 12 | "devDependencies": { 13 | "coveralls": "^3.0.2", 14 | "eslint": "^7.0.0", 15 | "eslint-config-prettier": "^7.0.0", 16 | "eslint-config-standard": "^16.0.0", 17 | "eslint-plugin-import": "^2.17.3", 18 | "eslint-plugin-mocha": "^8.0.0", 19 | "eslint-plugin-node": "^11.0.0", 20 | "eslint-plugin-promise": "^4.0.1", 21 | "eslint-plugin-standard": "^5.0.0", 22 | "fontkit": "^1.8.1", 23 | "mocha": "^7.0.0", 24 | "nyc": "^15.0.0", 25 | "offline-github-changelog": "^2.0.0", 26 | "prettier": "~2.2.0", 27 | "unexpected": "^12.0.0" 28 | }, 29 | "scripts": { 30 | "lint": "eslint . && prettier --check '**/*.{js,json,md}'", 31 | "test": "mocha", 32 | "test:ci": "npm run coverage", 33 | "coverage": "nyc --reporter=lcov --reporter=text --all -- npm test && echo google-chrome coverage/lcov-report/index.html", 34 | "preversion": "offline-github-changelog --next=${npm_new_version} > CHANGELOG.md && git add CHANGELOG.md" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/papandreou/subset-font.git" 39 | }, 40 | "keywords": [ 41 | "font", 42 | "subset", 43 | "harfbuzz", 44 | "hb-subset", 45 | "fonttools", 46 | "truetype", 47 | "TTF", 48 | "WOFF", 49 | "WOFF2", 50 | "wasm" 51 | ], 52 | "author": "Andreas Lind ", 53 | "license": "BSD-3-Clause", 54 | "bugs": { 55 | "url": "https://github.com/papandreou/subset-font/issues" 56 | }, 57 | "homepage": "https://github.com/papandreou/subset-font#readme", 58 | "files": [ 59 | "index.js", 60 | "*.md" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const fontkit = require('fontkit'); 2 | const expect = require('unexpected') 3 | .clone() 4 | .addAssertion( 5 | ' [not] to include name field ', 6 | async (expect, fontBuffer, fieldName) => { 7 | expect.errorMode = 'nested'; 8 | expect( 9 | fontkit.create(fontBuffer).name.records, 10 | '[not] to have property', 11 | fieldName 12 | ); 13 | } 14 | ) 15 | .addAssertion( 16 | ' [not] to include code point ', 17 | async (expect, fontBuffer, codePoint) => { 18 | expect.errorMode = 'nested'; 19 | expect( 20 | fontkit.create(fontBuffer).characterSet, 21 | '[not] to contain', 22 | codePoint 23 | ); 24 | } 25 | ) 26 | .addAssertion( 27 | ' [not] to include chunks ', 28 | async (expect, fontBuffer, ...chunkNames) => { 29 | expect.errorMode = 'nested'; 30 | expect( 31 | Object.keys(fontkit.create(fontBuffer).directory.tables), 32 | '[not] to contain ', 33 | ...chunkNames 34 | ); 35 | } 36 | ); 37 | const subsetFont = require('..'); 38 | const fontverter = require('fontverter'); 39 | const { readFile } = require('fs').promises; 40 | const pathModule = require('path'); 41 | 42 | describe('subset-font', function () { 43 | describe('with a truetype font', function () { 44 | before(async function () { 45 | this.sfntFont = await readFile( 46 | pathModule.resolve(__dirname, '..', 'testdata', 'OpenSans.ttf') 47 | ); 48 | }); 49 | 50 | describe('when not supplying the subset text as a string', function () { 51 | it('should fail with an error', async function () { 52 | await expect( 53 | () => subsetFont(this.sfntFont, ['f', 'o', 'o']), 54 | 'to error with', 55 | 'The subset text must be given as a string' 56 | ); 57 | }); 58 | }); 59 | 60 | describe('with no targetFormat given', function () { 61 | it('should return the subset as truetype', async function () { 62 | const result = await subsetFont(this.sfntFont, 'abcd'); 63 | 64 | expect(result, 'to be a', 'Buffer'); 65 | expect(result.length, 'to be less than', this.sfntFont.length); 66 | expect(fontverter.detectFormat(result), 'to equal', 'sfnt'); 67 | }); 68 | }); 69 | 70 | it('should produce a subset as ttf', async function () { 71 | const result = await subsetFont(this.sfntFont, 'abcd', { 72 | targetFormat: 'truetype', 73 | }); 74 | 75 | expect(result, 'to be a', 'Buffer'); 76 | expect(result.length, 'to be less than', this.sfntFont.length); 77 | expect(fontverter.detectFormat(result), 'to equal', 'sfnt'); 78 | }); 79 | 80 | it('should produce a subset as woff', async function () { 81 | const result = await subsetFont(this.sfntFont, 'abcd', { 82 | targetFormat: 'woff', 83 | }); 84 | 85 | expect(result, 'to be a', 'Buffer'); 86 | expect(result.length, 'to be less than', this.sfntFont.length); 87 | expect(result.slice(0, 4).toString(), 'to equal', 'wOFF'); 88 | }); 89 | 90 | it('should produce a subset as woff2', async function () { 91 | const result = await subsetFont(this.sfntFont, 'abcd', { 92 | targetFormat: 'woff2', 93 | }); 94 | 95 | expect(result, 'to be a', 'Buffer'); 96 | expect(result.length, 'to be less than', this.sfntFont.length); 97 | expect(result.slice(0, 4).toString(), 'to equal', 'wOF2'); 98 | }); 99 | 100 | describe('when not preserving any name ids', function () { 101 | it('should not preserve name id 14', async function () { 102 | const result = await subsetFont(this.sfntFont, 'abcd'); 103 | await expect(result, 'not to include name field', 'licenseURL'); 104 | }); 105 | }); 106 | 107 | describe('when preserving only name id 14', function () { 108 | before(async function () { 109 | this.result = await subsetFont(this.sfntFont, 'abcd', { 110 | preserveNameIds: [14], 111 | }); 112 | }); 113 | 114 | it('should preserve name id 14', async function () { 115 | await expect(this.result, 'to include name field', 'licenseURL'); 116 | }); 117 | }); 118 | 119 | // https://github.com/papandreou/subset-font/issues/15 120 | it('should handle surrogate pairs', async function () { 121 | const emojiFont = await readFile( 122 | pathModule.resolve(__dirname, '..', 'testdata', 'emoji.ttf') 123 | ); 124 | const result = await subsetFont(emojiFont, '\u{1d11e}'); // aka '\ud834\udd1e' 125 | await expect(result, 'to include code point', 0x1d11e); 126 | }); 127 | }); 128 | 129 | describe('with a woff font', function () { 130 | before(async function () { 131 | this.woffFont = await readFile( 132 | pathModule.resolve( 133 | __dirname, 134 | '..', 135 | 'testdata', 136 | 'k3k702ZOKiLJc3WVjuplzHhCUOGz7vYGh680lGh-uXM.woff' 137 | ) 138 | ); 139 | }); 140 | 141 | describe('with no targetFormat given', function () { 142 | it('should return the subset as woff', async function () { 143 | const result = await subsetFont(this.woffFont, 'abcd'); 144 | 145 | expect(result, 'to be a', 'Buffer'); 146 | expect(result.length, 'to be less than', this.woffFont.length); 147 | expect(result.slice(0, 4).toString(), 'to equal', 'wOFF'); 148 | }); 149 | }); 150 | 151 | it('should produce a subset as ttf', async function () { 152 | const result = await subsetFont(this.woffFont, 'abcd', { 153 | targetFormat: 'truetype', 154 | }); 155 | 156 | expect(result, 'to be a', 'Buffer'); 157 | expect(result.length, 'to be less than', this.woffFont.length); 158 | expect(fontverter.detectFormat(result), 'to equal', 'sfnt'); 159 | }); 160 | 161 | it('should produce a subset as woff', async function () { 162 | const result = await subsetFont(this.woffFont, 'abcd', { 163 | targetFormat: 'woff', 164 | }); 165 | 166 | expect(result, 'to be a', 'Buffer'); 167 | expect(result.length, 'to be less than', this.woffFont.length); 168 | expect(result.slice(0, 4).toString(), 'to equal', 'wOFF'); 169 | }); 170 | 171 | it('should produce a subset as woff2', async function () { 172 | const result = await subsetFont(this.woffFont, 'abcd', { 173 | targetFormat: 'woff2', 174 | }); 175 | 176 | expect(result, 'to be a', 'Buffer'); 177 | expect(result.length, 'to be less than', this.woffFont.length); 178 | expect(result.slice(0, 4).toString(), 'to equal', 'wOF2'); 179 | }); 180 | }); 181 | 182 | describe('with a woff2 font', function () { 183 | before(async function () { 184 | this.woff2Font = await readFile( 185 | pathModule.resolve(__dirname, '..', 'testdata', 'Roboto-400.woff2') 186 | ); 187 | }); 188 | 189 | describe('with no targetFormat given', function () { 190 | it('should return the subset as woff2', async function () { 191 | const result = await subsetFont(this.woff2Font, 'abcd'); 192 | 193 | expect(result, 'to be a', 'Buffer'); 194 | expect(result.length, 'to be less than', this.woff2Font.length); 195 | expect(result.slice(0, 4).toString(), 'to equal', 'wOF2'); 196 | }); 197 | }); 198 | 199 | it('should produce a subset as ttf', async function () { 200 | const result = await subsetFont(this.woff2Font, 'abcd', { 201 | targetFormat: 'truetype', 202 | }); 203 | 204 | expect(result, 'to be a', 'Buffer'); 205 | expect(result.length, 'to be less than', this.woff2Font.length); 206 | expect( 207 | result.slice(0, 4).toString('ascii'), 208 | 'to equal', 209 | '\x00\x01\x00\x00' 210 | ); 211 | }); 212 | 213 | it('should produce a subset as woff', async function () { 214 | const result = await subsetFont(this.woff2Font, 'abcd', { 215 | targetFormat: 'woff', 216 | }); 217 | 218 | expect(result, 'to be a', 'Buffer'); 219 | expect(result.length, 'to be less than', this.woff2Font.length); 220 | expect(result.slice(0, 4).toString(), 'to equal', 'wOFF'); 221 | }); 222 | 223 | it('should produce a subset as woff2', async function () { 224 | const result = await subsetFont(this.woff2Font, 'abcd', { 225 | targetFormat: 'woff2', 226 | }); 227 | 228 | expect(result, 'to be a', 'Buffer'); 229 | expect(result.length, 'to be less than', this.woff2Font.length); 230 | expect(result.slice(0, 4).toString(), 'to equal', 'wOF2'); 231 | }); 232 | }); 233 | 234 | describe('with a huge OTF font', function () { 235 | before(async function () { 236 | this.hugeOtfFont = await readFile( 237 | pathModule.resolve( 238 | __dirname, 239 | '..', 240 | 'testdata', 241 | 'SourceHanSerifCN-SemiBold.otf' 242 | ) 243 | ); 244 | }); 245 | 246 | it('should not crash when subsetting', async function () { 247 | const result = await subsetFont(this.hugeOtfFont, 'abcd'); 248 | 249 | expect(result, 'to be a', 'Buffer'); 250 | expect(result.length, 'to be less than', this.hugeOtfFont.length); 251 | expect(result.slice(0, 4).toString(), 'to equal', 'OTTO'); 252 | await expect(result, 'to include code point', 'a'.charCodeAt(0)); 253 | }); 254 | }); 255 | 256 | // https://github.com/papandreou/subset-font/issues/22 257 | describe('with an icon font', function () { 258 | before(async function () { 259 | this.materialIconsFont = await readFile( 260 | pathModule.resolve( 261 | __dirname, 262 | '..', 263 | 'testdata', 264 | 'MaterialIcons-Regular.ttf' 265 | ) 266 | ); 267 | }); 268 | 269 | describe('without the noLayoutClosure flag', function () { 270 | it('should retain more glyphs', async function () { 271 | const result = await subsetFont( 272 | this.materialIconsFont, 273 | `_abcdefghijklmnopqrstuvwxyz0123456789${String.fromCodePoint( 274 | 0xe5c5, 275 | 0xe5c8 276 | )}` 277 | ); 278 | 279 | expect(result.length, 'to be greater than', 300000); 280 | }); 281 | }); 282 | 283 | describe('with the noLayoutClosure flag', function () { 284 | it('should retain more glyphs', async function () { 285 | const result = await subsetFont( 286 | this.materialIconsFont, 287 | `_abcdefghijklmnopqrstuvwxyz0123456789${String.fromCodePoint( 288 | 0xe5c5, 289 | 0xe5c8 290 | )}`, 291 | { 292 | noLayoutClosure: true, 293 | } 294 | ); 295 | 296 | expect(result.length, 'to be less than', 3000); 297 | }); 298 | }); 299 | }); 300 | 301 | describe('with a truncated font', function () { 302 | before(async function () { 303 | this.truncatedTtfFont = ( 304 | await readFile( 305 | pathModule.resolve(__dirname, '..', 'testdata', 'FZZJ-ZSXKJW.ttf') 306 | ) 307 | ).slice(0, 131072); 308 | }); 309 | 310 | it('should error out', async function () { 311 | await expect( 312 | subsetFont(this.truncatedTtfFont, 'abcd', { 313 | targetFormat: 'woff', 314 | }), 315 | 'to be rejected with', 316 | 'hb_subset_or_fail (harfbuzz) returned zero, indicating failure. Maybe the input file is corrupted?' 317 | ); 318 | }); 319 | }); 320 | 321 | describe('when omitting or preserving tables from the subsetted font', function () { 322 | beforeEach(async function () { 323 | // This font file contains these tables: 324 | // Feat, GDEF, GPOS, GSUB, Glat, Gloc, OS/2, Silf, Sill, cmap, glyf, head, hhea, hmtx, loca, maxp, name, post 325 | this.paduakBookFont = await readFile( 326 | pathModule.resolve( 327 | __dirname, 328 | '..', 329 | 'testdata', 330 | 'PadaukBook-Regular.ttf' 331 | ) 332 | ); 333 | }); 334 | 335 | describe('with default options', function () { 336 | it('should include the (subsetted) standard tables used by web browsers', async function () { 337 | const result = await subsetFont(this.paduakBookFont, 'abcd'); 338 | 339 | await expect( 340 | result, 341 | 'to include chunks', 342 | 'GDEF', 343 | 'GPOS', 344 | 'GSUB', 345 | 'OS/2', 346 | 'cmap', 347 | 'glyf', 348 | 'head', 349 | 'hhea', 350 | 'hmtx', 351 | 'loca', 352 | 'maxp', 353 | 'name', 354 | 'post' 355 | ).and( 356 | 'not to include chunks', 357 | 'DSIG', 358 | 'BASE', 359 | 'Feat', 360 | 'Glat', 361 | 'Gloc', 362 | 'Silf', 363 | 'Sill' 364 | ); 365 | }); 366 | }); 367 | }); 368 | 369 | describe('with a variable font', function () { 370 | beforeEach(async function () { 371 | this.variableRobotoFont = await readFile( 372 | pathModule.resolve( 373 | __dirname, 374 | '..', 375 | 'testdata', 376 | 'RobotoFlex-VariableFont_GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf' 377 | ) 378 | ); 379 | }); 380 | 381 | describe('when not instancing the font using axis pinning', function () { 382 | it('should include the original variation axes', async function () { 383 | const result = await subsetFont(this.variableRobotoFont, 'abcd'); 384 | 385 | expect(fontkit.create(result).variationAxes, 'to satisfy', { 386 | wght: { name: 'wght', min: 100, default: 400, max: 1000 }, 387 | wdth: { name: 'wdth', min: 25, default: 100, max: 151 }, 388 | opsz: { name: 'opsz', min: 8, default: 14, max: 144 }, 389 | GRAD: { name: 'GRAD', min: -200, default: 0, max: 150 }, 390 | slnt: { name: 'slnt', min: -10, default: 0, max: 0 }, 391 | XTRA: { name: 'XTRA', min: 323, default: 468, max: 603 }, 392 | XOPQ: { name: 'XOPQ', min: 27, default: 96, max: 175 }, 393 | YOPQ: { name: 'YOPQ', min: 25, default: 79, max: 135 }, 394 | YTLC: { name: 'YTLC', min: 416, default: 514, max: 570 }, 395 | YTUC: { name: 'YTUC', min: 528, default: 712, max: 760 }, 396 | YTAS: { name: 'YTAS', min: 649, default: 750, max: 854 }, 397 | YTDE: { name: 'YTDE', min: -305, default: -203, max: -98 }, 398 | YTFI: { name: 'YTFI', min: 560, default: 738, max: 788 }, 399 | }); 400 | }); 401 | }); 402 | 403 | describe('when instancing the font using axis pinning', function () { 404 | describe('when pinning all the axes', function () { 405 | it('should remove the variation axes from the font', async function () { 406 | const result = await subsetFont(this.variableRobotoFont, 'abcd', { 407 | variationAxes: { 408 | wght: 200, 409 | wdth: 120, 410 | opsz: 80, 411 | GRAD: -20, 412 | slnt: -8, 413 | XTRA: 502, 414 | XOPQ: 101, 415 | YOPQ: 79, 416 | YTLC: 420, 417 | YTUC: 600, 418 | YTAS: 810, 419 | YTDE: -90, 420 | YTFI: 660, 421 | }, 422 | }); 423 | 424 | expect(fontkit.create(result).variationAxes, 'to equal', {}); 425 | 426 | // When not instancing the subset font is about 29 KB 427 | expect(result.length, 'to be less than', 4096); 428 | }); 429 | }); 430 | }); 431 | 432 | describe('when pinning only some of the axes', function () { 433 | it('should remove the pinned variation axes from the font', async function () { 434 | const result = await subsetFont(this.variableRobotoFont, 'abcd', { 435 | variationAxes: { 436 | wght: 200, 437 | wdth: 120, 438 | opsz: 80, 439 | XTRA: 502, 440 | XOPQ: 101, 441 | YOPQ: 79, 442 | YTLC: 420, 443 | YTUC: 600, 444 | YTAS: 810, 445 | YTDE: -90, 446 | YTFI: 660, 447 | }, 448 | }); 449 | 450 | expect(fontkit.create(result).variationAxes, 'to equal', { 451 | GRAD: { name: 'GRAD', min: -200, default: 0, max: 150 }, 452 | slnt: { name: 'slnt', min: -10, default: 0, max: 0 }, 453 | }); 454 | 455 | // When not instancing the subset font is about 29 KB 456 | expect(result.length, 'to be less than', 26000); 457 | }); 458 | }); 459 | 460 | describe('when reducing the ranges of some variation axes', function () { 461 | it('should perform a partial instancing', async function () { 462 | const result = await subsetFont(this.variableRobotoFont, 'abcd', { 463 | variationAxes: { 464 | GRAD: { min: -50, max: 50, default: 25 }, 465 | slnt: { min: -9, max: 0 }, 466 | YTDE: { min: -100, max: -98 }, 467 | opsz: 14, 468 | XTRA: 468, 469 | XOPQ: 96, 470 | YOPQ: 79, 471 | YTLC: 514, 472 | YTUC: 712, 473 | YTAS: 750, 474 | YTFI: 738, 475 | // Leaving out wght and wdth so that the full variation space is preserved 476 | }, 477 | }); 478 | 479 | expect( 480 | fontkit.create(result).variationAxes, 481 | 'to exhaustively satisfy', 482 | { 483 | GRAD: { name: 'GRAD', min: -50, max: 50, default: 25 }, 484 | slnt: { name: 'slnt', min: -9, max: 0, default: 0 }, 485 | YTDE: { name: 'YTDE', min: -100, max: -98, default: -100 }, 486 | wght: { name: 'wght', min: 100, max: 1000, default: 400 }, 487 | wdth: { name: 'wdth', min: 25, max: 151, default: 100 }, 488 | } 489 | ); 490 | 491 | // When not instancing the subset font is about 29 KB 492 | expect(result.length, 'to be less than', 25000); 493 | }); 494 | 495 | describe('when leaving out a min value', function () { 496 | it('should error', async function () { 497 | await expect( 498 | () => 499 | subsetFont(this.variableRobotoFont, 'abcd', { 500 | variationAxes: { 501 | wght: { max: 300 }, 502 | }, 503 | }), 504 | 'to error', 505 | 'wght: You must provide both a min and a max value when setting the axis range' 506 | ); 507 | }); 508 | }); 509 | 510 | describe('when leaving out a max value', function () { 511 | it('should error', async function () { 512 | await expect( 513 | () => 514 | subsetFont(this.variableRobotoFont, 'abcd', { 515 | variationAxes: { 516 | wght: { min: 300 }, 517 | }, 518 | }), 519 | 'to error', 520 | 'wght: You must provide both a min and a max value when setting the axis range' 521 | ); 522 | }); 523 | }); 524 | 525 | describe('when pinning a non-existent axis', function () { 526 | it('should error', async function () { 527 | await expect( 528 | () => 529 | subsetFont(this.variableRobotoFont, 'abcd', { 530 | variationAxes: { 531 | foob: 123, 532 | }, 533 | }), 534 | 'to error', 535 | 'hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning foob to 123, indicating failure. Maybe the axis does not exist in the font?' 536 | ); 537 | }); 538 | }); 539 | 540 | describe('when reducing the variation space of a non-existent axis', function () { 541 | it('should error', async function () { 542 | await expect( 543 | () => 544 | subsetFont(this.variableRobotoFont, 'abcd', { 545 | variationAxes: { 546 | foob: { 547 | min: 123, 548 | max: 456, 549 | }, 550 | }, 551 | }), 552 | 'to error', 553 | 'hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of foob to [123; 456] and a default value of undefined, indicating failure. Maybe the axis does not exist in the font?' 554 | ); 555 | }); 556 | }); 557 | }); 558 | }); 559 | }); 560 | -------------------------------------------------------------------------------- /testdata/FZZJ-ZSXKJW.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/FZZJ-ZSXKJW.ttf -------------------------------------------------------------------------------- /testdata/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /testdata/OpenSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/OpenSans.ttf -------------------------------------------------------------------------------- /testdata/PadaukBook-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/PadaukBook-Regular.ttf -------------------------------------------------------------------------------- /testdata/Roboto-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/Roboto-400.woff2 -------------------------------------------------------------------------------- /testdata/RobotoFlex-VariableFont_GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/RobotoFlex-VariableFont_GRAD,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght.ttf -------------------------------------------------------------------------------- /testdata/SourceHanSerifCN-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/SourceHanSerifCN-SemiBold.otf -------------------------------------------------------------------------------- /testdata/emoji.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/emoji.ttf -------------------------------------------------------------------------------- /testdata/k3k702ZOKiLJc3WVjuplzHhCUOGz7vYGh680lGh-uXM.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/papandreou/subset-font/2564b094e7ddc44051a95913074b7603bdfbfd46/testdata/k3k702ZOKiLJc3WVjuplzHhCUOGz7vYGh680lGh-uXM.woff --------------------------------------------------------------------------------