├── example ├── Roboto │ ├── bold.ttf │ ├── light.ttf │ ├── thin.ttf │ ├── regular.ttf │ ├── bold--italic.ttf │ ├── thin--italic.ttf │ ├── light--italic.ttf │ └── regular--italic.ttf └── config.js ├── .eslintrc.js ├── .editorconfig ├── .github └── workflows │ ├── publish.yml │ └── node.js.yml ├── lib ├── cli.js ├── default-config.js └── index.js ├── .gitignore ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.ru.md ├── README.md ├── logo-top.svg └── logo-bottom.svg /example/Roboto/bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/bold.ttf -------------------------------------------------------------------------------- /example/Roboto/light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/light.ttf -------------------------------------------------------------------------------- /example/Roboto/thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/thin.ttf -------------------------------------------------------------------------------- /example/Roboto/regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/regular.ttf -------------------------------------------------------------------------------- /example/Roboto/bold--italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/bold--italic.ttf -------------------------------------------------------------------------------- /example/Roboto/thin--italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/thin--italic.ttf -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@funboxteam', 3 | env: { 4 | node: true, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/Roboto/light--italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/light--italic.ttf -------------------------------------------------------------------------------- /example/Roboto/regular--italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funbox/beatrix/HEAD/example/Roboto/regular--italic.ttf -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: '18.15.0' 19 | registry-url: 'https://registry.npmjs.org' 20 | scope: '@funboxteam' 21 | - run: npm ci 22 | - run: npm publish 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const program = require('commander'); 4 | const beatrix = require('.'); 5 | 6 | program 7 | .usage('-c [-o ] ') 8 | .version(require('../package.json').version, '-v, --version') 9 | .description(` 10 | A tool for converting and optimizing fonts. 11 | 12 | Prerequisite: 13 | $ pip install fonttools zopfli brotli 14 | `); 15 | 16 | program 17 | .option('-c, --config ', 'path to config file (default: common ASCII symbols and some typographic ones)') 18 | .option('-o, --output ', 'path to the output dir (default: ./dist)') 19 | .parse(process.argv); 20 | 21 | if (!program.args.length) { 22 | console.log('You have to pass paths to files or dirs with font files.'); 23 | process.exit(1); 24 | } 25 | 26 | beatrix({ 27 | configPath: program.config, 28 | outputPath: program.output, 29 | typefacesDirPath: program.args && program.args[0], 30 | }); 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- Node.js -- 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Dependency directory 9 | /node_modules 10 | 11 | # Optional npm cache directory 12 | .npm 13 | 14 | 15 | # -- VSCode -- 16 | /.vscode 17 | jsconfig.json 18 | 19 | 20 | # -- IDEA -- 21 | /.idea 22 | 23 | ## File-based project format: 24 | *.ipr 25 | *.iws 26 | *.iml 27 | 28 | 29 | # -- Vim -- 30 | 31 | # swap 32 | [._]*.s[a-w][a-z] 33 | [._]s[a-w][a-z] 34 | 35 | # session 36 | Session.vim 37 | 38 | # temporary 39 | .netrwhist 40 | *~ 41 | /tmp 42 | 43 | 44 | # -- Eclipse -- 45 | *.swp 46 | 47 | 48 | # -- OS X -- 49 | .DS_Store 50 | 51 | 52 | # -- Public -- 53 | /public 54 | /public-sandbox 55 | 56 | 57 | # -- Tests -- 58 | /test-reports 59 | /test-logs 60 | /gemini-* 61 | /screenshots 62 | 63 | 64 | # -- Sass -- 65 | .sass-cache 66 | 67 | 68 | # -- ESLint -- 69 | .eslintcache 70 | 71 | # -- Stylelint -- 72 | .stylelintcache 73 | 74 | 75 | # -- Dist -- 76 | dist 77 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | # npm of 14.x is not compatible with lockfileVersion 3, that's why we use `install` here instead of `ci` 28 | - if: matrix.node-version == '14.x' 29 | run: npm install 30 | - if: matrix.node-version != '14.x' 31 | run: npm ci 32 | - run: npm run build --if-present 33 | - run: npm run lint 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Igor Adamenko and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/default-config.js: -------------------------------------------------------------------------------- 1 | const CHARACTERS = [ 2 | // space, !, ", #, $, %, &, ', (, ), *, +, comma, -, dot, /, 3 | // numbers, :, ;, <, =, >, ?, @, A-Z, [, \, ], ^, _, `, a-z, 4 | // {, |, }, ~ 5 | '0020-007E', 6 | 7 | // non-breaking space 8 | '00A0', 9 | 10 | // © 11 | '00A9', 12 | 13 | // soft hyphen 14 | '00AD', 15 | 16 | // middle dot (·) 17 | '00B7', 18 | 19 | // en space 20 | '2002', 21 | 22 | // em space 23 | '2003', 24 | 25 | // thin space 26 | '2009', 27 | 28 | // hair space 29 | '200A', 30 | 31 | // hyphen (-) 32 | '2010', 33 | 34 | // non-breaking hyphen 35 | '2011', 36 | 37 | // en dash (–) 38 | '2013', 39 | 40 | // em dash (—) 41 | '2014', 42 | 43 | // ‘, ’, ‚ (single low-9 quotation mark), ‛, “, ”, „ 44 | '2018-201E', 45 | 46 | // bullet (•) 47 | '2022', 48 | 49 | // ellipsis (…) 50 | '2026', 51 | 52 | // narrow non-breaking space 53 | '202F', 54 | 55 | // minus (−) 56 | '2212', 57 | ]; 58 | 59 | // OpenType features 60 | // https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist 61 | const LAYOUT_FEATURES = ['kern', 'lnum', 'tnum']; 62 | 63 | module.exports = { 64 | CHARACTERS, 65 | LAYOUT_FEATURES, 66 | }; 67 | -------------------------------------------------------------------------------- /example/config.js: -------------------------------------------------------------------------------- 1 | const CHARACTERS = [ 2 | // space, !, ", #, $, %, &, ', (, ), *, +, comma, -, dot, /, 3 | // numbers, :, ;, <, =, >, ?, @, A-Z, [, \, ], ^, _, `, a-z, 4 | // {, |, }, ~ 5 | '0020-007E', 6 | 7 | // non-breaking space 8 | '00A0', 9 | 10 | // © 11 | '00A9', 12 | 13 | // « 14 | '00AB', 15 | 16 | // » 17 | '00BB', 18 | 19 | // soft hyphen 20 | '00AD', 21 | 22 | // middle dot (·) 23 | '00B7', 24 | 25 | // Ё 26 | '0401', 27 | 28 | // А-Я, а-я 29 | '0410-044F', 30 | 31 | // ё 32 | '0451', 33 | 34 | // en space 35 | '2002', 36 | 37 | // em space 38 | '2003', 39 | 40 | // thin space 41 | '2009', 42 | 43 | // hair space 44 | '200A', 45 | 46 | // hyphen (-) 47 | '2010', 48 | 49 | // non-breaking hyphen 50 | '2011', 51 | 52 | // en dash (–) 53 | '2013', 54 | 55 | // em dash (—) 56 | '2014', 57 | 58 | // ‘, ’, ‚ (single low-9 quotation mark), ‛, “, ”, „ 59 | '2018-201E', 60 | 61 | // bullet (•) 62 | '2022', 63 | 64 | // ellipsis (…) 65 | '2026', 66 | 67 | // narrow non-breaking space 68 | '202F', 69 | 70 | // rouble sign (₽) 71 | '20BD', 72 | 73 | // minus (−) 74 | '2212', 75 | ]; 76 | 77 | module.exports = { 78 | CHARACTERS, 79 | }; 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@funboxteam/beatrix", 3 | "version": "3.0.1", 4 | "description": "A tool for converting and optimizing font files", 5 | "author": "Igor Adamenko (https://igoradamenko.com)", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/funbox/beatrix" 10 | }, 11 | "main": "./lib/index.js", 12 | "bin": { 13 | "beatrix": "./lib/cli.js" 14 | }, 15 | "files": [ 16 | "lib/", 17 | "CHANGELOG.md", 18 | "README.md" 19 | ], 20 | "scripts": { 21 | "lint": "eslint -c .eslintrc.js lib", 22 | "test": "./lib/cli.js --config ./example/config.js --output ./dist ./example", 23 | "prepublishOnly": "if [ -z \"$CI\" ]; then lawyer; fi", 24 | "pretest": "npm run lint" 25 | }, 26 | "dependencies": { 27 | "commander": "4.1.1" 28 | }, 29 | "devDependencies": { 30 | "@funboxteam/eslint-config": "7.3.0", 31 | "eslint": "7.32.0", 32 | "husky": "3.0.7", 33 | "lint-staged": "9.4.0" 34 | }, 35 | "lint-staged": { 36 | "lib/**/*.js": [ 37 | "eslint --fix --cache -c .eslintrc.js", 38 | "git add" 39 | ] 40 | }, 41 | "husky": { 42 | "hooks": { 43 | "pre-commit": "lint-staged -q" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.1 (20.06.2023) 4 | 5 | Fixed the way Beatrix works with file paths, just to be sure that it won't blow up the directories outside of its domain. 6 | 7 | Also ensured that the tool works properly on Node.js 18. 8 | 9 | 10 | ## 3.0.0 (02.05.2023) 11 | 12 | Added `lnum` into the list of supported OpenType features. 13 | 14 | No need to migrate anything, but your font files may become bigger a bit. 15 | Override `LAYOUT_FEATURES` config option if you want to turn some OpenType features off. 16 | 17 | 18 | ## 2.1.0 (11.10.2021) 19 | 20 | Now it's possible to pass desired layout features using config file. 21 | 22 | List the features you want to keep using the `LAYOUT_FEATURES` config field. 23 | See the [default config](https://github.com/funbox/beatrix/blob/1c645bd42e686eb5818831be86f41003bc56ff0d/lib/default-config.js#L65) for example. 24 | 25 | 26 | ## 2.0.1 (10.06.2021) 27 | 28 | Fixed several security vulnerabilities: 29 | 30 | - [Use of a Broken or Risky Cryptographic Algorithm](https://github.com/advisories/GHSA-r9p9-mrjm-926w) in [elliptic](https://github.com/indutny/elliptic). Updated from 6.5.3 to 6.5.4. 31 | 32 | - [Regular Expression Denial of Service](https://github.com/advisories/GHSA-43f8-2h32-f4cj) in [hosted-git-info](https://github.com/npm/hosted-git-info). Updated from 2.8.8 to 2.8.9. 33 | 34 | - [Command Injection](https://github.com/advisories/GHSA-35jh-r3h4-6jhm) in [lodash](https://github.com/lodash/lodash). Updated from 4.17.19 to 4.17.21. 35 | 36 | - [Regular Expression Denial of Service](https://www.npmjs.com/advisories/1751) in [glob-parent](https://www.npmjs.com/package/glob-parent). Updated from 5.1.1 to 5.1.2. 37 | 38 | 39 | ## 2.0.0 (25.09.2020) 40 | 41 | The package was fully refactored to make it ready to be published on GitHub. 42 | 43 | As a result: 44 | 1. CLI was added. 45 | 2. Demo page generation was removed. 46 | 3. Styles files generation was removed. 47 | 4. Russian and other Cyrillic symbols were removed from the default config. 48 | 5. Font files loading scheme was highly simplified. Now the source directory may have any structure. 49 | 50 | Finally, the MIT license file was added. 51 | 52 | There is no migration guide for this update, because the purpose of the package was changed, 53 | and 2.0.0 is a completely new thing. 54 | 55 | 56 | ## 1.2.0 (13.07.2020) 57 | 58 | Added kern option into the list of enabled font features, to enhance font kerning. 59 | 60 | 61 | ## 1.1.0 (27.01.2020) 62 | 63 | Added [tnum](https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#tag-tnum) option 64 | into the list of enabled font features, to make `font-variant-numeric: tabular-nums` CSS rule work. 65 | 66 | 67 | ## 1.0.0 (16.01.2020) 68 | 69 | Initial version. 70 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { execFileSync } = require('child_process'); 4 | 5 | const DEFAULT_CONFIG = require('./default-config'); 6 | 7 | const DEFAULT_OUTPUT_DIR = './dist'; 8 | 9 | module.exports = beatrix; 10 | 11 | function beatrix({ configPath, outputPath, typefacesDirPath }) { 12 | if (!typefacesDirPath) { 13 | throw new Error('You have to pass the path to dir with font files.'); 14 | } 15 | 16 | configPath = configPath && getAbsPath(configPath); 17 | outputPath = getAbsPath(outputPath || DEFAULT_OUTPUT_DIR); 18 | typefacesDirPath = getAbsPath(typefacesDirPath); 19 | 20 | const config = configPath 21 | // it's fine here, we're loading user's config file 22 | // eslint-disable-next-line import/no-dynamic-require 23 | ? { ...DEFAULT_CONFIG, ...require(configPath) } 24 | : DEFAULT_CONFIG; 25 | 26 | const charactersCodes = config.CHARACTERS; 27 | const charactersString = charactersCodes.join(','); 28 | 29 | const features = config.LAYOUT_FEATURES; 30 | const featuresString = features.join(','); 31 | 32 | const fontFiles = loadFontFiles(typefacesDirPath); 33 | 34 | if (!fontFiles.length) { 35 | throw new Error(`Font files not found in ${typefacesDirPath}.`); 36 | } 37 | 38 | execFileSync('rm', ['-rf', outputPath]); 39 | fs.mkdirSync(outputPath); 40 | console.log('Output dir cleared.'); 41 | 42 | fontFiles.forEach(srcTTFPath => { 43 | console.log('------------------------'); 44 | console.log(`Start processing '${srcTTFPath}'...`); 45 | 46 | const srcDirPath = path.dirname(srcTTFPath); 47 | const destDirPath = srcDirPath.replace(typefacesDirPath, outputPath); 48 | 49 | const srcFilename = path.basename(srcTTFPath, path.extname(srcTTFPath)); 50 | const destPathBase = path.join(destDirPath, srcFilename); 51 | 52 | fs.mkdirSync(destDirPath, { recursive: true }); 53 | console.log(`Dest dir created: '${destDirPath}'.`); 54 | 55 | const destPathTTF = `${destPathBase}.ttf`; 56 | const destPathWOFF = `${destPathBase}.woff`; 57 | const destPathWOFF2 = `${destPathBase}.woff2`; 58 | 59 | // we don't need TTF to inject on webpages, but generate it 60 | // just because we can 61 | convert(srcTTFPath, destPathTTF, charactersString, featuresString); 62 | 63 | console.log(`TTF subset: ${buildTTFSubsetStat(srcTTFPath, destPathTTF)}`); 64 | console.log('TTF created.'); 65 | 66 | convert(srcTTFPath, destPathWOFF, charactersString, featuresString); 67 | console.log('WOFF created.'); 68 | 69 | convert(srcTTFPath, destPathWOFF2, charactersString, featuresString); 70 | console.log('WOFF2 created.'); 71 | 72 | console.log(`Completed processing '${srcTTFPath}'.`); 73 | }); 74 | 75 | console.log('------------------------'); 76 | 77 | console.log('Done.'); 78 | } 79 | 80 | function convert(src, dest, charactersString, featuresString) { 81 | const args = [ 82 | // input file 83 | src, 84 | 85 | // output file 86 | `--output-file=${dest}`, 87 | 88 | // flavour of output font file. may be 'woff' or 'woff2' 89 | ...(dest.match(/\.woff$/) ? ['--flavor=woff'] : []), 90 | ...(dest.match(/\.woff2$/) ? ['--flavor=woff2'] : []), 91 | 92 | // use the google zopfli algorithm to compress WOFF. 93 | // the output is 3-8% smaller than pure zlib, but the compression speed is 94 | // much slower 95 | ...(dest.match(/\.woff$/) ? ['--with-zopfli'] : []), 96 | 97 | // OpenType features, e.g., `kern`, `liga`, `tnum`, etc. 98 | // https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist 99 | `--layout-features=${featuresString}`, 100 | 101 | // make the font unusable as a system font by replacing name IDs 1, 2, 3, 4, 102 | // and 6 with dummy strings (it is still fully functional as webfont) 103 | '--obfuscate-names', 104 | 105 | // comma/whitespace-separated list of Unicode codepoints or ranges 106 | // as hex numbers, optionally prefixed with 'U+', 'u', etc. 107 | // example: --unicodes=U+0041-005A,U+0061-007A 108 | `--unicodes=${charactersString}`, 109 | ]; 110 | 111 | execFileSync('pyftsubset', args); 112 | } 113 | 114 | function loadFontFiles(dir) { 115 | if (dir.startsWith('.')) return []; 116 | 117 | try { 118 | return fs.readdirSync(dir).reduce((acc, p) => acc.concat(loadFontFiles(path.join(dir, p))), []); 119 | } catch (err) { 120 | if (err.code === 'ENOTDIR') { 121 | return isFontFile(dir) ? [dir] : []; 122 | } 123 | 124 | return []; 125 | } 126 | } 127 | 128 | function isFontFile(filepath) { 129 | return filepath.endsWith('.ttf') || filepath.endsWith('.otf'); 130 | } 131 | 132 | function buildTTFSubsetStat(srcTTFPath, destPathTTF) { 133 | const prevTTFSize = Math.ceil(fs.statSync(srcTTFPath).size / 1024); 134 | const currentTTFSize = Math.ceil(fs.statSync(destPathTTF).size / 1024); 135 | 136 | return `${prevTTFSize} Kb → ${currentTTFSize} Kb (−${Math.floor(100 - currentTTFSize / prevTTFSize * 100)}%).`; 137 | } 138 | 139 | function getAbsPath(p) { 140 | return path.resolve(process.cwd(), p); 141 | } 142 | -------------------------------------------------------------------------------- /README.ru.md: -------------------------------------------------------------------------------- 1 |
2 | Логотип Беатрикс: слово “BEATRIX” разрезанное на две части катаной 3 |
4 | 5 |
6 | Beatrix отрезает ненужные части шрифтов и конвертирует TTF/OTF в WOFF и WOFF2 7 |
8 | 9 | ## Мотивация 10 | 11 | Пользователи тратят огромное количество времени на скачивание веб-ассетов: JS, CSS, изображения, шрифты и так далее. 12 | Чтобы как-то уменьшить это время, разработчики сжимают ассеты, гзипают их, используют оптимизированные форматы для картинок и шрифтов. 13 | 14 | Но иногда разработчики могут пойти ещё чуть дальше. Если у них есть права на используемые шрифты, 15 | то они могут оставить только те глифы, которые нужны на их сайтах. А остальные можно и вырезать. 16 | 17 | Беатрикс как раз для этого и нужна. 18 | 19 | ## Установка 20 | 21 | Беатрикс — это обёртка вокруг Пайтон-скриптов из набора [fonttools](https://github.com/fonttools/fonttools). 22 | Потому сперва нужно установить их и зависимости для них: 23 | 24 | ```sh 25 | pip install fonttools zopfli brotli 26 | ``` 27 | 28 | А затем саму Беатрикс: 29 | 30 | ```sh 31 | npm install --save @funboxteam/beatrix 32 | ``` 33 | 34 | Готово. 35 | 36 | ## Использование 37 | 38 | Беатрикс ожидает, что вы передадите ей папку с TTF/OTF-файлами и конфигурационный файл с перечислением символов, 39 | которые нужно оставить в шрифтах. 40 | 41 | Так, если вы склонируете этот репозиторий и установите всё неоходимое, сможете протестировать работу Беатрикс так: 42 | 43 | ```sh 44 | beatrix --config ./example/config.js --output ./dist ./example 45 | ``` 46 | 47 | Она загрузит `./example/config.js`, найдёт все TTF/OTF-файлы внутри `./example`, оптимизирует их, конвертирует в WOFF и WOFF2, 48 | и положит всё в `./dist`. 49 | 50 |
51 | Вывод этой команды 52 | 53 | ```text 54 | $ beatrix --config ./example/config.js --output ./dist ./example 55 | Output dir cleared. 56 | ------------------------ 57 | Start processing '/tmp/beatrix/example/Roboto/bold--italic.ttf'... 58 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 59 | TTF subset: 171 Kb → 33 Kb (−80%). 60 | TTF created. 61 | WOFF created. 62 | WOFF2 created. 63 | Completed processing '/tmp/beatrix/example/Roboto/bold--italic.ttf'. 64 | ------------------------ 65 | Start processing '/tmp/beatrix/example/Roboto/bold.ttf'... 66 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 67 | TTF subset: 167 Kb → 32 Kb (−80%). 68 | TTF created. 69 | WOFF created. 70 | WOFF2 created. 71 | Completed processing '/tmp/beatrix/example/Roboto/bold.ttf'. 72 | ------------------------ 73 | Start processing '/tmp/beatrix/example/Roboto/light--italic.ttf'... 74 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 75 | TTF subset: 173 Kb → 34 Kb (−80%). 76 | TTF created. 77 | WOFF created. 78 | WOFF2 created. 79 | Completed processing '/tmp/beatrix/example/Roboto/light--italic.ttf'. 80 | ------------------------ 81 | Start processing '/tmp/beatrix/example/Roboto/light.ttf'... 82 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 83 | TTF subset: 167 Kb → 32 Kb (−80%). 84 | TTF created. 85 | WOFF created. 86 | WOFF2 created. 87 | Completed processing '/tmp/beatrix/example/Roboto/light.ttf'. 88 | ------------------------ 89 | Start processing '/tmp/beatrix/example/Roboto/regular--italic.ttf'... 90 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 91 | TTF subset: 170 Kb → 33 Kb (−80%). 92 | TTF created. 93 | WOFF created. 94 | WOFF2 created. 95 | Completed processing '/tmp/beatrix/example/Roboto/regular--italic.ttf'. 96 | ------------------------ 97 | Start processing '/tmp/beatrix/example/Roboto/regular.ttf'... 98 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 99 | TTF subset: 168 Kb → 32 Kb (−80%). 100 | TTF created. 101 | WOFF created. 102 | WOFF2 created. 103 | Completed processing '/tmp/beatrix/example/Roboto/regular.ttf'. 104 | ------------------------ 105 | Start processing '/tmp/beatrix/example/Roboto/thin--italic.ttf'... 106 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 107 | TTF subset: 172 Kb → 34 Kb (−80%). 108 | TTF created. 109 | WOFF created. 110 | WOFF2 created. 111 | Completed processing '/tmp/beatrix/example/Roboto/thin--italic.ttf'. 112 | ------------------------ 113 | Start processing '/tmp/beatrix/example/Roboto/thin.ttf'... 114 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 115 | TTF subset: 168 Kb → 32 Kb (−80%). 116 | TTF created. 117 | WOFF created. 118 | WOFF2 created. 119 | Completed processing '/tmp/beatrix/example/Roboto/thin.ttf'. 120 | ------------------------ 121 | Done. 122 | ``` 123 |
124 | 125 | ### Конфигурационный файл 126 | 127 | Конфиг — это JS- или JSON-файл, описывающий объект с ключом `CHARACTERS` и значением в виде массива, каждый элемент которого 128 | описывает один или несколько Unicode-символов. Каждый Unicode-символ представлен в виде четырёх шестнадцатиричных цифр. 129 | 130 | Например: 131 | 132 | ```js 133 | module.exports = { 134 | CHARACTERS: [ 135 | // Описывает ряд символов с U+0020 по U+007E включительно. 136 | // А именно: пробел, !, ", #, $, %, &, ', (, ), *, +, запятую, 137 | // -, точку, /, числа, :, ;, <, =, >, ?, @, A-Z, [, \, ], ^, 138 | // _, `, a-z, {, |, }, ~ 139 | '0020-007E', 140 | 141 | // Один Unicode-символ 142 | // неразравный пробел 143 | '00A0', 144 | 145 | // Ещё один Unicode-символ 146 | // © 147 | '00A9', 148 | 149 | // ... 150 | ] 151 | } 152 | ``` 153 | 154 | Символы, описанные выше, будут оставлены в шрифтовых файлах (как и кернинговые пары для них), а остальные будут вырезаны. 155 | 156 | [![Sponsored by FunBox](logo-bottom.svg)](https://funbox.ru) 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Beatrix logo: “BEATRIX” cut into two pieces by katana 3 |
4 | 5 |
6 | Beatrix chops off useless fonts parts and converts TTF/OTF files into WOFF & WOFF2 7 |
8 | 9 | ## Rationale 10 | 11 | Users spend a lot of time downloading web assets: JS, CSS, images, fonts, etc. 12 | To reduce waiting time, developers compress the assets, gzip them, use optimized formats for images and fonts. 13 | 14 | But sometimes the developers can go a little bit further. When they have all the rights for fonts they use, 15 | it's possible to leave only the glyphs their website needs. Just cut off the rest ones. 16 | 17 | That's exactly how the tool works. 18 | 19 | ## Getting Started 20 | 21 | [По-русски](./README.ru.md) 22 | 23 | First of all, the tool is a wrapper around Python scripts distributed as [fonttools](https://github.com/fonttools/fonttools). 24 | You should install them and the deps they need to: 25 | 26 | ```sh 27 | pip install fonttools zopfli brotli 28 | ``` 29 | 30 | Then you can install Beatrix: 31 | 32 | ```sh 33 | npm install --save @funboxteam/beatrix 34 | ``` 35 | 36 | Now you're ready to optimize your fonts. 37 | 38 | ## Usage 39 | 40 | Beatrix expects to get a path to directory with TTF/OTF files inside and the config with allowed characters listed. 41 | 42 | E.g. if you clone this repo and install the tool, you will be able to run it like this: 43 | 44 | ```sh 45 | beatrix --config ./example/config.js --output ./dist ./example 46 | ``` 47 | 48 | It will load `./example/config.js`, find all the TTF/OTF files inside `./example`, optimize & convert them to WOFF & WOFF2, 49 | and put the results into `./dist`. 50 | 51 |
52 | The output of this command 53 | 54 | ```text 55 | $ beatrix --config ./example/config.js --output ./dist ./example 56 | Output dir cleared. 57 | ------------------------ 58 | Start processing '/tmp/beatrix/example/Roboto/bold--italic.ttf'... 59 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 60 | TTF subset: 171 Kb → 33 Kb (−80%). 61 | TTF created. 62 | WOFF created. 63 | WOFF2 created. 64 | Completed processing '/tmp/beatrix/example/Roboto/bold--italic.ttf'. 65 | ------------------------ 66 | Start processing '/tmp/beatrix/example/Roboto/bold.ttf'... 67 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 68 | TTF subset: 167 Kb → 32 Kb (−80%). 69 | TTF created. 70 | WOFF created. 71 | WOFF2 created. 72 | Completed processing '/tmp/beatrix/example/Roboto/bold.ttf'. 73 | ------------------------ 74 | Start processing '/tmp/beatrix/example/Roboto/light--italic.ttf'... 75 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 76 | TTF subset: 173 Kb → 34 Kb (−80%). 77 | TTF created. 78 | WOFF created. 79 | WOFF2 created. 80 | Completed processing '/tmp/beatrix/example/Roboto/light--italic.ttf'. 81 | ------------------------ 82 | Start processing '/tmp/beatrix/example/Roboto/light.ttf'... 83 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 84 | TTF subset: 167 Kb → 32 Kb (−80%). 85 | TTF created. 86 | WOFF created. 87 | WOFF2 created. 88 | Completed processing '/tmp/beatrix/example/Roboto/light.ttf'. 89 | ------------------------ 90 | Start processing '/tmp/beatrix/example/Roboto/regular--italic.ttf'... 91 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 92 | TTF subset: 170 Kb → 33 Kb (−80%). 93 | TTF created. 94 | WOFF created. 95 | WOFF2 created. 96 | Completed processing '/tmp/beatrix/example/Roboto/regular--italic.ttf'. 97 | ------------------------ 98 | Start processing '/tmp/beatrix/example/Roboto/regular.ttf'... 99 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 100 | TTF subset: 168 Kb → 32 Kb (−80%). 101 | TTF created. 102 | WOFF created. 103 | WOFF2 created. 104 | Completed processing '/tmp/beatrix/example/Roboto/regular.ttf'. 105 | ------------------------ 106 | Start processing '/tmp/beatrix/example/Roboto/thin--italic.ttf'... 107 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 108 | TTF subset: 172 Kb → 34 Kb (−80%). 109 | TTF created. 110 | WOFF created. 111 | WOFF2 created. 112 | Completed processing '/tmp/beatrix/example/Roboto/thin--italic.ttf'. 113 | ------------------------ 114 | Start processing '/tmp/beatrix/example/Roboto/thin.ttf'... 115 | Dest dir created: '/tmp/beatrix/dist/Roboto'. 116 | TTF subset: 168 Kb → 32 Kb (−80%). 117 | TTF created. 118 | WOFF created. 119 | WOFF2 created. 120 | Completed processing '/tmp/beatrix/example/Roboto/thin.ttf'. 121 | ------------------------ 122 | Done. 123 | ``` 124 |
125 | 126 | ### Config file 127 | 128 | Config is a JS or JSON file which describes an object containing `CHARACTERS` and `LAYOUT_FEATURES`. 129 | 130 | `CHARACTERS` is an array, where each item is a string describing one Unicode number or a range of them. 131 | Each Unicode number is represented as 4 hex digits. 132 | 133 | `LAYOUT_FEATURES` is an array of [OpenType features](https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist) (e.g., `kern`, `liga`, `tnum`, etc.). 134 | 135 | **Please note** that if no config provided, the [default config](lib/default-config.js) is used. If one of the options is not present in the config, the appropriate option from the default config is used. 136 | 137 | Example: 138 | 139 | ```js 140 | module.exports = { 141 | CHARACTERS: [ 142 | // Unicode range from U+0020 to U+007E (including the last one). 143 | // Contains: space, !, ", #, $, %, &, ', (, ), *, +, comma, 144 | // -, dot, /, numbers, :, ;, <, =, >, ?, @, A-Z, [, \, ], ^, 145 | // _, `, a-z, {, |, }, ~ 146 | '0020-007E', 147 | 148 | // One Unicode number 149 | // non-breaking space 150 | '00A0', 151 | 152 | // One more Unicode number 153 | // © 154 | '00A9', 155 | 156 | // ... 157 | ], 158 | 159 | // Drop all the features except 'tnum' and 'kern' 160 | LAYOUT_FEATURES: ['tnum', 'kern'] 161 | } 162 | ``` 163 | 164 | The characters and features described above will be left in the font files, all the rest will be cut off. 165 | 166 | If you want to remove all the characters or features completely just pass an empty array. 167 | 168 | [![Sponsored by FunBox](logo-bottom.svg)](https://funbox.ru) 169 | -------------------------------------------------------------------------------- /logo-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /logo-bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | --------------------------------------------------------------------------------