├── .editorconfig ├── .eslintrc.yml ├── .github ├── auto-comment.yml └── workflows │ └── ci.yml ├── .gitignore ├── .posthtmlrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── doc └── font_spec.md ├── lib ├── app_error.js ├── cli.js ├── collect_font_data.js ├── convert.js ├── font │ ├── cmap_build_subtables.js │ ├── compress.js │ ├── font.js │ ├── table_cmap.js │ ├── table_glyf.js │ ├── table_head.js │ ├── table_kern.js │ └── table_loca.js ├── freetype │ ├── build │ │ └── ft_render.js │ ├── index.js │ └── render.c ├── ranger.js ├── utils.js └── writers │ ├── bin.js │ ├── dump.js │ └── lvgl │ ├── index.js │ ├── lv_font.js │ ├── lv_table_cmap.js │ ├── lv_table_glyf.js │ ├── lv_table_head.js │ └── lv_table_kern.js ├── lv_font_conv.js ├── package-lock.json ├── package.json ├── support ├── Dockerfile ├── build.sh └── build_web.js ├── test ├── .eslintrc.yml ├── font │ ├── fixtures │ │ ├── font_info_AV.json │ │ └── font_info_AV_size200.json │ ├── test_cmap_build_subtables.js │ ├── test_compress.js │ └── test_font.js ├── test_cli.js ├── test_collect_font_data.js ├── test_ranger.js └── test_utils.js └── web ├── .eslintrc.yml ├── .htmlhintrc ├── content.html ├── index.html └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Cross-editor coding style settings. 2 | # See http://editorconfig.org/ for details. 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | 5 | parserOptions: 6 | ecmaVersion: '2018' 7 | 8 | ignorePatterns: 9 | - dist 10 | - lib/freetype/build 11 | 12 | rules: 13 | accessor-pairs: 2 14 | array-bracket-spacing: [ 2, "always", { "singleValue": true, "objectsInArrays": true, "arraysInArrays": true } ] 15 | block-scoped-var: 2 16 | block-spacing: 2 17 | brace-style: [ 2, '1tbs', { "allowSingleLine": true } ] 18 | # Postponed 19 | #callback-return: 2 20 | comma-dangle: 2 21 | comma-spacing: 2 22 | comma-style: 2 23 | computed-property-spacing: [ 2, never ] 24 | # Postponed 25 | #consistent-return: 2 26 | consistent-this: [ 2, self ] 27 | # ? change to multi 28 | curly: [ 2, 'multi-line' ] 29 | # Postponed 30 | # dot-notation: [ 2, { allowKeywords: true } ] 31 | dot-location: [ 2, 'property' ] 32 | eol-last: 2 33 | eqeqeq: 2 34 | func-style: [ 2, declaration ] 35 | # Postponed 36 | #global-require: 2 37 | guard-for-in: 2 38 | handle-callback-err: 2 39 | 40 | # Postponed 41 | indent: [ 2, 2, { VariableDeclarator: { var: 2, let: 2, const: 3 }, SwitchCase: 1 } ] 42 | 43 | # key-spacing: [ 2, { "align": "value" } ] 44 | keyword-spacing: 2 45 | linebreak-style: 2 46 | max-depth: [ 1, 3 ] 47 | max-nested-callbacks: [ 1, 5 ] 48 | # string can exceed 80 chars, but should not overflow github website :) 49 | max-len: [ 2, 120, 1000 ] 50 | new-cap: 0 51 | new-parens: 2 52 | # Postponed 53 | #newline-after-var: 2 54 | no-alert: 2 55 | no-array-constructor: 2 56 | no-caller: 2 57 | #no-case-declarations: 2 58 | no-catch-shadow: 2 59 | no-cond-assign: 2 60 | no-console: 1 61 | no-constant-condition: 2 62 | no-control-regex: 2 63 | no-debugger: 1 64 | no-delete-var: 2 65 | no-div-regex: 2 66 | no-dupe-args: 2 67 | no-dupe-keys: 2 68 | no-duplicate-case: 2 69 | no-else-return: 2 70 | # Tend to drop 71 | # no-empty: 1 72 | no-empty-character-class: 2 73 | no-empty-pattern: 2 74 | no-eq-null: 2 75 | no-eval: 2 76 | no-ex-assign: 2 77 | no-extend-native: 2 78 | no-extra-bind: 2 79 | no-extra-boolean-cast: 2 80 | no-extra-semi: 2 81 | no-fallthrough: 2 82 | no-floating-decimal: 2 83 | no-func-assign: 2 84 | # Postponed 85 | #no-implicit-coercion: [2, { "boolean": true, "number": true, "string": true } ] 86 | no-implied-eval: 2 87 | no-inner-declarations: 2 88 | no-invalid-regexp: 2 89 | no-irregular-whitespace: 2 90 | no-iterator: 2 91 | no-label-var: 2 92 | no-labels: 2 93 | no-lone-blocks: 1 94 | no-lonely-if: 2 95 | no-loop-func: 2 96 | no-mixed-requires: [ 1, { "grouping": true } ] 97 | no-mixed-spaces-and-tabs: 2 98 | # Postponed 99 | #no-native-reassign: 2 100 | no-negated-in-lhs: 2 101 | # Postponed 102 | #no-nested-ternary: 2 103 | no-new: 2 104 | no-new-func: 2 105 | no-new-object: 2 106 | no-new-require: 2 107 | no-new-wrappers: 2 108 | no-obj-calls: 2 109 | no-octal: 2 110 | no-octal-escape: 2 111 | no-path-concat: 2 112 | no-proto: 2 113 | no-redeclare: 2 114 | # Postponed 115 | #no-regex-spaces: 2 116 | no-return-assign: 2 117 | no-self-compare: 2 118 | no-sequences: 2 119 | # Postponed 120 | #no-shadow: 2 121 | no-shadow-restricted-names: 2 122 | no-sparse-arrays: 2 123 | # Postponed 124 | #no-sync: 2 125 | no-trailing-spaces: 2 126 | no-undef: 2 127 | no-undef-init: 2 128 | no-unexpected-multiline: 2 129 | no-unreachable: 2 130 | no-unused-expressions: 2 131 | no-unused-vars: 2 132 | no-use-before-define: 2 133 | no-void: 2 134 | no-with: 2 135 | object-curly-spacing: [ 2, always, { "objectsInObjects": true, "arraysInObjects": true } ] 136 | operator-assignment: 1 137 | # Postponed 138 | #operator-linebreak: [ 2, after ] 139 | semi: 2 140 | semi-spacing: 2 141 | space-before-function-paren: [ 2, { "anonymous": "always", "named": "never" } ] 142 | space-in-parens: [ 2, never ] 143 | space-infix-ops: 2 144 | space-unary-ops: 2 145 | # Postponed 146 | #spaced-comment: [ 1, always, { exceptions: [ '/', '=' ] } ] 147 | strict: [ 2, global ] 148 | quotes: [ 2, single, avoid-escape ] 149 | quote-props: [ 1, 'as-needed' ] 150 | radix: 2 151 | use-isnan: 2 152 | valid-typeof: 2 153 | yoda: [ 2, never, { "exceptRange": true } ] 154 | 155 | # 156 | # es6 157 | # 158 | arrow-body-style: [ 1, "as-needed" ] 159 | arrow-parens: [ 1, "as-needed" ] 160 | arrow-spacing: 2 161 | constructor-super: 2 162 | generator-star-spacing: [ 2, {"before": false, "after": true } ] 163 | no-class-assign: 2 164 | no-confusing-arrow: [ 1, { allowParens: true } ] 165 | no-const-assign: 2 166 | #no-constant-condition: 2 167 | no-dupe-class-members: 2 168 | no-this-before-super: 2 169 | # Postponed 170 | #no-var: 2 171 | object-shorthand: 1 172 | # Postponed 173 | #prefer-arrow-callback: 1 174 | # Postponed 175 | #prefer-const: 1 176 | #prefer-reflect 177 | #prefer-spread 178 | # Postponed 179 | #prefer-template: 1 180 | require-yield: 1 181 | -------------------------------------------------------------------------------- /.github/auto-comment.yml: -------------------------------------------------------------------------------- 1 | # Comment to a new issue. 2 | pullRequestOpened: | 3 | Thank you for raising your pull request. 4 | 5 | To ensure that all licensing criteria is met all repositories of the LVGL project apply a process called DCO (Developer's Certificate of Origin). 6 | 7 | The text of DCO can be read here: https://developercertificate.org/ 8 | For a more detailed description see the [Documentation](https://docs.lvgl.io/latest/en/html/contributing/index.html#developer-certification-of-origin-dco) site. 9 | 10 | By contributing to any repositories of the LVGL project you state that your contribution corresponds with the DCO. 11 | 12 | No further action is required if your contribution fulfills the DCO. If you are not sure about it feel free to ask us in a comment. 13 | -------------------------------------------------------------------------------- /.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 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [ '14' ] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions/setup-node@v2 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | dist 5 | .cache 6 | *.log 7 | *.swp 8 | .vscode 9 | -------------------------------------------------------------------------------- /.posthtmlrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'posthtml-include': { 4 | root: './web' 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.5.2 / 2021-07-18 2 | ------------------ 3 | 4 | - Fixed lvgl version check for v8+, #64. 5 | 6 | 7 | 1.5.1 / 2021-04-06 8 | ------------------ 9 | 10 | - Fixed fail of CMAP generation for edge cases, #62. 11 | - Dev deps bump. 12 | 13 | 14 | 1.5.0 / 2021-03-08 15 | ------------------ 16 | 17 | - More `const` in generated font (for v8+), #59. 18 | 19 | 20 | 1.4.1 / 2021-01-26 21 | ------------------ 22 | 23 | - Fix charcodes padding in comments, #54. 24 | 25 | 26 | 1.4.0 / 2021-01-03 27 | ------------------ 28 | 29 | - Added OTF fonts support. 30 | - Added `--use-color-info` for limited multi-tone glyphs support. 31 | 32 | 33 | 1.3.1 / 2020-12-28 34 | ------------------ 35 | 36 | - Unify `lvgl.h` include. 37 | - Updated repo refs (littlevgl => lvgl). 38 | - Deps bump. 39 | - Moved CI to github actions. 40 | 41 | 42 | 1.3.0 / 2020-10-25 43 | ------------------ 44 | 45 | - Drop `lodash` use. 46 | - Deps bump. 47 | 48 | 49 | 1.2.1 / 2020-10-24 50 | ------------------ 51 | 52 | - Reduced npm package size (drop unneeded files before publish). 53 | 54 | 55 | 1.2.0 / 2020-10-24 56 | ------------------ 57 | 58 | - Bump FreeType to 2.10.4. 59 | - Bundle dependencies to npm package. 60 | 61 | 62 | 1.1.3 / 2020-09-22 63 | ------------------ 64 | 65 | - lvgl: added `LV_FONT_FMT_TXT_LARGE` check or very large fonts. 66 | 67 | 68 | 1.1.2 / 2020-08-23 69 | ------------------ 70 | 71 | - Fix: skip `glyph.advanceWidth` for monospace fonts, #43. 72 | - Spec fix: version size should be 4 bytes, #44. 73 | - Spec fix: bbox x/y bits => unsigned, #45. 74 | - Bump argparse. 75 | - Cleanup help formatter. 76 | 77 | 78 | 1.1.1 / 2020-08-01 79 | ------------------ 80 | 81 | - `--version` should show number from `package.json`. 82 | 83 | 84 | 1.1.0 / 2020-07-27 85 | ------------------ 86 | 87 | - Added `post.underlinePosition` & `post.underlineThickness` info to font header. 88 | 89 | 90 | 1.0.0 / 2020-06-26 91 | ------------------ 92 | 93 | - Maintenance release. 94 | - Set package version 1.x, to label package as stable. 95 | - Deps bump. 96 | 97 | 98 | 0.4.3 / 2020-03-05 99 | ------------------ 100 | 101 | - Enabled `--bpp 8` mode. 102 | 103 | 104 | 0.4.2 / 2020-01-05 105 | ------------------ 106 | 107 | - Added `--lv_include` option to set alternate `lvgl.h` path. 108 | - Added guards to hide `.subpx` property for lvgl 6.0 (supported from 6.1 only), #32. 109 | - Dev deps bump 110 | 111 | 112 | 0.4.1 / 2019-12-09 113 | ------------------ 114 | 115 | - Allow memory growth for FreeType build, #29. 116 | - Dev deps bump. 117 | - Web build update. 118 | 119 | 120 | 0.4.0 / 2019-11-29 121 | ------------------ 122 | 123 | - Note, this release is for lvgl 6.1 and has potentially breaking changes 124 | (see below). If you have compatibility issues with lvgl 6.0 - use previous 125 | versions or update your code. 126 | - Spec change: added subpixels info field to font header (header size increased). 127 | - Updated `bin` & `lvgl` writers to match new spec. 128 | - lvgl: fixed data type for kerning values (needs appropriate update 129 | in LittlevGL 6.1+). 130 | - Fix errors display (disable emscripten error catcher). 131 | 132 | 133 | 0.3.1 / 2019-10-24 134 | ------------------ 135 | 136 | - Fixed "out of range" error for big `--size`. 137 | 138 | 139 | 0.3.0 / 2019-10-12 140 | ------------------ 141 | 142 | - Added beta options `--lcd` & `--lcd-v` for subpixel rendering (still need 143 | header info update). 144 | - Added FreeType data properties to dump info. 145 | - Fixed glyph width (missed fractional part after switch to FreeType). 146 | - Fixed missed sigh for negative X/Y bitmap offsets. 147 | - Deps bump. 148 | 149 | 150 | 0.2.0 / 2019-09-26 151 | ------------------ 152 | 153 | - Use FreeType renderer. Should solve all regressions, reported in 0.1.0. 154 | - Enforced light autohinting (horizontal lines only). 155 | - Use special hinter for monochrome output (improve quality). 156 | - API changed to async. 157 | - Fix: added missed `.bitmap_format` field to lvgl writer. 158 | - Fix: changed struct fields init order to match declaration, #25. 159 | 160 | 161 | 0.1.0 / 2019-09-03 162 | ------------------ 163 | 164 | - First release. 165 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 authors 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lv_font_conv - font convertor to compact bitmap format 2 | ====================================================== 3 | 4 | [![CI](https://github.com/lvgl/lv_font_conv/workflows/CI/badge.svg?branch=master)](https://github.com/lvgl/lv_font_conv/actions) 5 | [![NPM version](https://img.shields.io/npm/v/lv_font_conv.svg?style=flat)](https://www.npmjs.org/package/lv_font_conv) 6 | 7 | Converts TTF/WOFF/OTF fonts to __[compact format](https://github.com/lvgl/lv_font_conv/blob/master/doc/font_spec.md)__, suitable for small embedded systems. Main features are: 8 | 9 | - Allows bitonal and anti-aliased glyphs (1-4 bits per pixel). 10 | - Preserves kerning info. 11 | - Compression. 12 | - Users can select required glyphs only (subsetting). 13 | - Multiple font sources can be merged. 14 | - Simple CLI interface, easy to integrate into external build systems. 15 | 16 | 17 | ## Install the script 18 | 19 | [node.js](https://nodejs.org/en/download/) v14+ required. 20 | 21 | Global install of the last version, execute as "lv_font_conv" 22 | 23 | ```sh 24 | # install release from npm registry 25 | npm i lv_font_conv -g 26 | # install from github's repo, master branch 27 | npm i lvgl/lv_font_conv -g 28 | ``` 29 | 30 | **run via [npx](https://www.npmjs.com/package/npx) without install** 31 | 32 | ```sh 33 | # run from npm registry 34 | npx lv_font_conv -h 35 | # run from github master 36 | npx github:lvgl/lv_font_conv -h 37 | ``` 38 | 39 | Note, running via `npx` may take some time until modules installed, be patient. 40 | 41 | 42 | ## CLI params 43 | 44 | Common: 45 | 46 | - `--bpp` - bits per pixel (antialiasing). 47 | - `--size` - output font size (pixels). 48 | - `-o`, `--output` - output path (file or directory, depends on format). 49 | - `--format` - output format. 50 | - `--format dump` - dump glyph images and font info, useful for debug. 51 | - `--format bin` - dump font in binary form (as described in [spec](https://github.com/lvgl/lv_font_conv/blob/master/doc/font_spec.md)). 52 | - `--format lvgl` - dump font in [LVGL](https://github.com/lvgl/lvgl) format. 53 | - `--force-fast-kern-format` - always use more fast kering storage format, 54 | at cost of some size. If size difference appears, it will be displayed. 55 | - `--lcd` - generate bitmaps with 3x horizontal resolution, for subpixel 56 | smoothing. 57 | - `--lcd-v` - generate bitmaps with 3x vertical resolution, for subpixel 58 | smoothing. 59 | - `--use-color-info` - try to use glyph color info from font to create 60 | grayscale icons. Since gray tones are emulated via transparency, result 61 | will be good on contrast background only. 62 | - `--lv-include` - only with `--format lvgl`, set alternate path for `lvgl.h`. 63 | - `--no-compress` - disable built-in RLE compression. 64 | - `--no-prefilter` - disable bitmap lines filter (XOR), used to improve 65 | compression ratio. 66 | - `--byte-align` - pad the line ends of the bitmaps to a whole byte (requires `--no-compress` and `--bpp != 3`) 67 | - `--no-kerning` - drop kerning info to reduce size (not recommended). 68 | 69 | Per font: 70 | 71 | - `--font` - path to font file (ttf/woff/woff2/otf). May be used multiple time for 72 | merge. 73 | - `-r`, `--range` - single glyph or range + optional mapping, belongs to 74 | previously declared `--font`. Can be used multiple times. Examples: 75 | - `-r 0x1F450` - single value, dec or hex format. 76 | - `-r 0x1F450-0x1F470` - range. 77 | - `-r '0x1F450=>0xF005'` - single glyph with mapping. 78 | - `-r '0x1F450-0x1F470=>0xF005'` - range with mapping. 79 | - `-r 0x1F450 -r 0x1F451-0x1F470` - 2 ranges. 80 | - `-r 0x1F450,0x1F451-0x1F470` - the same as above, but defined with single `-r`. 81 | - `--symbols` - list of characters to copy (instead of numeric format in `-r`). 82 | - `--symbols 0123456789.,` - extract chars to display numbers. 83 | - `--autohint-off` - do not force autohinting ("light" is on by default). 84 | - `--autohint-strong` - use more strong autohinting (will break kerning). 85 | 86 | Additional debug options: 87 | - `--full-info` - don't shorten 'font_info.json' (include pixels data). 88 | 89 | 90 | ## Examples 91 | 92 | Merge english from Roboto Regular and icons from Font Awesome, and show debug 93 | info: 94 | 95 | `env DEBUG=* lv_font_conv --font Roboto-Regular.ttf -r 0x20-0x7F --font FontAwesome.ttf -r 0xFE00=>0x81 --size 16 --format bin --bpp 3 --no-compress -o output.font` 96 | 97 | Merge english & russian from Roboto Regular, and show debug info: 98 | 99 | `env DEBUG=* lv_font_conv --font Roboto-Regular.ttf -r 0x20-0x7F -r 0x401,0x410-0x44F,0x451 --size 16 --format bin --bpp 3 --no-compress -o output.font` 100 | 101 | Dump all Roboto glyphs to inspect icons and font details: 102 | 103 | `lv_font_conv --font Roboto-Regular.ttf -r 0x20-0x7F --size 16 --format dump --bpp 3 -o ./dump` 104 | 105 | **Note**. Option `--no-compress` exists temporary, to avoid confusion until LVGL 106 | adds compression support. 107 | 108 | 109 | ## Technical notes 110 | 111 | ### Supported output formats 112 | 113 | 1. **bin** - universal binary format, as described in https://github.com/lvgl/lv_font_conv/tree/master/doc. 114 | 2. **lvgl** - format for LVGL, C file. Has minor limitations and a bit 115 | bigger size, because C does not allow to effectively define relative offsets 116 | in data blocks. 117 | 3. **dump** - create folder with each glyph in separate image, and other font 118 | data as `json`. Useful for debug. 119 | 120 | ### Merged font metrics 121 | 122 | When multiple fonts merged into one, sources can have different metrics. Result 123 | will follow principles below: 124 | 125 | 1. No scaling. Glyphs will have exactly the same size, as intended by font authors. 126 | 2. The same baseline. 127 | 3. `OS/2` metrics (`sTypoAscender`, `sTypoDescender`, `sTypoLineGap`) will be 128 | used from the first font in list. 129 | 4. `hhea` metrics (`ascender`, `descender`), defined as max/min point of all 130 | font glyphs, are recalculated, according to new glyphs set. 131 | 132 | 133 | ## Development 134 | 135 | Current package includes WebAssembly build of FreeType with some helper 136 | functions. Everything is wrapped into Docker and requires zero knowledge about 137 | additional tools install. See `package.json` for additional commands. You may 138 | need those if decide to upgrade FreeType or update helpers. 139 | 140 | This builds image with emscripten & freetype, usually should be done only once: 141 | 142 | ``` 143 | npm run build:dockerimage 144 | ``` 145 | 146 | This compiles helpers and creates WebAssembly files: 147 | 148 | ``` 149 | npm run build:freetype 150 | ``` 151 | -------------------------------------------------------------------------------- /doc/font_spec.md: -------------------------------------------------------------------------------- 1 | # Bitmap fonts format for embedded systems 2 | 3 | This spec defines binary bitmap fonts format, usable for embedded systems with 4 | constrained resources. Main features are: 5 | 6 | - Kerning support (existing BDF/PCF fonts don't have it). 7 | - Different bits per pixel (1..4). Usable for bi-tonal and anti-aliased fonts. 8 | - Highly optimized storage size (keep bounding-box data only, with built-in 9 | compression). 10 | 11 | Based on https://docs.microsoft.com/en-us/typography/opentype/spec/, 12 | but simplified for bitmap fonts: 13 | 14 | - No separate global header, everything in 'head' table 15 | - `advanceWidth` placed in `glyf` table 16 | - Total glyphs count limited to 65536 (char codes are not limited, up to 0x10FFFF 17 | supported). 18 | - No vertical fonts. 19 | - No ligatures. 20 | 21 | All multi-byte numbers are stored in LE (little-endian) order. 22 | 23 | ## Table: `head` (font header) 24 | 25 | [Initial Reference](https://docs.microsoft.com/en-us/typography/opentype/spec/head) 26 | 27 | Global values. 28 | 29 | Size (bytes) | Description 30 | -------------|------------ 31 | 4 | Record size (for quick skip) 32 | 4 | `head` (table marker) 33 | 4 | Version (reserved) 34 | 2 | Number of additional tables (2 bytes to simplify align) 35 | 2 | Font size (px), as defined in convertor params 36 | 2 | Ascent (uint16), as returned by `Font.ascender` of `opentype.js` (usually HHead ascent) 37 | 2 | Descent (int16, negative), as returned by `Font.descender` of `opentype.js` (usually HHead descent) 38 | 2 | typoAscent (uint16), typographic ascent 39 | 2 | typoDescent (int16), typographic descent 40 | 2 | typoLineGap (uint16), typographic line gap 41 | 2 | min Y (used to quick check line intersections with other objects) 42 | 2 | max Y 43 | 2 | default advanceWidth (uint16), if glyph advanceWidth bits length = 0 44 | 2 | kerningScale, FP12.4 unsigned, scale for kerning data, to fit source in 1 byte 45 | 1 | indexToLocFormat in `loca` table (`0` - Offset16, `1` - Offset32) 46 | 1 | glyphIdFormat (`0` - 1 byte, `1` - 2 bytes) 47 | 1 | advanceWidthFormat (`0` - Uint, `1` - unsigned with 4 bits fractional part) 48 | 1 | Bits per pixel (1, 2, 3 or 4) 49 | 1 | Glyph BBox x/y bits length (unsigned) 50 | 1 | Glyph BBox w/h bits length (unsigned) 51 | 1 | Glyph advanceWidth bits length (unsigned, may be FP4) 52 | 1 | Compression alg ID (0 - raw bits, 1 - RLE-like with XOR prefilter, 2 - RLE-like only without prefilter) 53 | 1 | Subpixel rendering. `0` - none, `1` - horisontal resolution of bitmaps is 3x, `2` - vertical resolution of bitmaps is 3x. 54 | 1 | Reserved (align to 2x) 55 | 2 | Underline position (int16), scaled `post.underlinePosition` 56 | 2 | Underline thickness (uint16), scaled `post.underlineThickness` 57 | x | Unused (Align header length to 4x) 58 | 59 | Note, `Ascent + abs(Descent)` may be NOT equal to font size. 60 | 61 | 62 | ## Table: `cmap` 63 | 64 | [Initial Reference](https://docs.microsoft.com/en-us/typography/opentype/spec/cmap) 65 | 66 | Map code points to internal IDs. Consists of optimized "subtables" for compact 67 | data store. 68 | 69 | Differs from original by list of fixed subtable headers for quick lookup. 70 | Subtable formats implemented only partially. 71 | 72 | Size (bytes) | Description 73 | -------------|------------ 74 | 4 | Record size (for quick skip) 75 | 4 | `cmap` (table marker) 76 | 4 | Subtables count (4 to simplify align) 77 | 16 | Subtable 1 header 78 | 16 | Subtable 2 header 79 | ...|... 80 | ? | Subtable 1 data (aligned to 4) 81 | ? | Subtable 2 data (aligned to 4) 82 | ...|... 83 | 84 | All subtables are non intersecting ranges, headers and content 85 | are ordered by codePoint for fast scan or binary search. 86 | 87 | ### Subtable header 88 | 89 | Size (bytes) | Description 90 | -------------|------------ 91 | 4 | Data offset (or 0 if data segment not exists) 92 | 4 | Range start (min codePoint) 93 | 2 | Range length (up to 65535) 94 | 2 | Glyph ID offset (for delta-coding) 95 | 2 | Data entries count (for sparse data) 96 | 1 | Format type (`0` => format 0, `1` => format sparse, `2` => format 0 tiny, `3` => format sparse tiny) 97 | 1 | - (align to 4) 98 | 99 | ### Subtable "format 0" data 100 | 101 | [Initial Reference](https://docs.microsoft.com/en-us/typography/opentype/spec/cmap#format-0-byte-encoding-table) 102 | 103 | `Array[uint8]` (continuous, delta-coded), or empty data. 104 | 105 | - Index = codePoint - (Min codePoint) 106 | - Map to Glyph ID as `Value + Glyph ID offset`. 107 | 108 | bytes | description 109 | ------|------------ 110 | 1 | delta-encoded Glyph ID for (range_start + 0) codePoint 111 | 1 | delta-encoded Glyph ID for (range_start + 1) codePoint 112 | ... | ... 113 | 1 | delta-encoded Glyph ID for (range_end) codePoint 114 | 115 | "Missed" chars are mapped to 0 116 | 117 | 118 | ### Subtable "format sparse" data 119 | 120 | For non continuous sets (CJK subsets, for example). `Array[entries]` of 121 | delta-coded codepoints + `Array[entries]` of delta-coded glyph IDs. 122 | 123 | bytes | description 124 | ------|------------ 125 | 2 | (codePoint1 - range_start) 126 | 2 | (codePoint2 - range_start) 127 | ...|... 128 | 2 |(last codepoint - range_start) 129 | 2 | delta-encoded Glyph1 ID 130 | 2 | delta-encoded Glyph2 ID 131 | ... | ... 132 | 2 | delta-encoded last glyph ID 133 | 134 | 135 | ### Subtable "format 0 tiny" 136 | 137 | Special case of "format 0", without IDs index. 138 | 139 | In most of cases, glyph IDs will be consecutive and have no gaps. Then we can 140 | calculate ID value as `glyph ID offset + codepoint index`. 141 | 142 | In total, this format will have header only, without data. 143 | 144 | 145 | ### Subtable "format sparse tiny" 146 | 147 | Exactly as "format sparse", but without glyph IDs index at the end 148 | (with codepoints only). 149 | 150 | See "format 0 tiny" for explanations. 151 | 152 | 153 | ## Table: `loca` 154 | 155 | [Initial Reference](https://docs.microsoft.com/en-us/typography/opentype/spec/loca) 156 | 157 | Data offsets in `glyf` table for each glyph id. Can be `Offset16` or `Offset32`. 158 | Type is defined in `head` table. 159 | 160 | Size (bytes) | Description 161 | -------------|------------ 162 | 4 | Record size (for quick skip) 163 | 4 | `loca` (table marker) 164 | 4 | Entries count (4 to simplify slign) 165 | 2 or 4 | id1 offset 166 | 2 or 4 | id2 offset 167 | ... | ... 168 | 169 | 170 | ## Table: `glyf` 171 | 172 | [Initial Reference](https://docs.microsoft.com/en-us/typography/opentype/spec/glyf) 173 | 174 | Contains glyph bitmap data. Data offsets for each id defined in `loca` table. 175 | 176 | Coordinate system is `(right, up)` - point `(0, 0)` located at (baseline, left) 177 | corner - as in OpenType fonts. Glyph BBox's `(x, y)` defines bottom left corner. 178 | 179 | Image inside of BBox is drawn from top left corner, to right and down. 180 | 181 | Size (bytes) | Description 182 | -------------|------------ 183 | 4 | Record size (for quick skip) 184 | 4 | `glyf` (table marker) 185 | ?? | Glyph id1 data 186 | ?? | Glyph id2 data 187 | ...| ... 188 | 189 | Note, Glyph ID 0 is reserved for "undefined". It's recommended set it to 190 | 0xFFFE char image. 191 | 192 | **Note**. Glyph data is NOT aligned (now) and should be loaded byte-by-byte. 193 | 194 | 195 | ### Glyph data 196 | 197 | Stream of bits. 198 | 199 | Note, bounding box is NOT related to typographic area (height * advanceWidth), 200 | and can be shifted anyhow. Real image can overflow typographic space. 201 | 202 | Size (bits) | Description 203 | ------------|------------ 204 | NN | advanceWidth (length/format in font header, may have 4 fractional bits) 205 | NN | BBox X (length in font header) 206 | NN | BBox Y (see length in font header 207 | NN | BBox Width (see length in font header) 208 | NN | BBox Height (see length in font header) 209 | ?? | Compressed bitmap 210 | 211 | If bitmaps are generated for subpixel rendering, then BBox Width or BBox Height 212 | value will be 3x more than "normal" one. They always contain real size of 213 | content, not rendered size. 214 | 215 | 216 | ## Table `kern` 217 | 218 | Initial References: [1](https://docs.microsoft.com/en-us/typography/opentype/spec/kern), 219 | [2](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html) 220 | 221 | Kerning info is optional. Only a very small subset of `kern`/`GPOS` features is 222 | supported: 223 | 224 | - Horisontal kerning only. 225 | - 2 subtables formats, for compact store (sorted pairs and table of glyph classes). 226 | - No feature stacking (combination of subtables). `kern` table contain just data 227 | in one of supported formats. 228 | 229 | Stored kerning values are - always FP4.4 fixed point with sign. It should be 230 | multiplied to FP12.4 `kerningScale` from font header. Note, 7 bits resolution is 231 | enough for our simple needs. But kerning of fonts > 40px can exceed max value 232 | of signed FP4.4. So `kerningScale` allows properly scale covered range. 233 | 234 | Data layout: 235 | 236 | Size (bytes) | Description 237 | -------------|------------ 238 | 4 | Record size (for quick skip) 239 | 4 | `kern` (table marker) 240 | 1 | Format type (0 & 3 now supported) 241 | 3 | - (align) 242 | ?? | format content 243 | 244 | ### Format 0 (Sorted pairs) 245 | 246 | Just a sorted list of `(id_left, id_right, value)`, where id's are combined into 247 | `uint32_t`, and binary searchable. This format may provide some saving for 248 | `ascii` set (english), but become very ineffective for multiple languages. 249 | 250 | Unlike original, id pairs and values are stored separate to simplify align. 251 | 252 | Content: 253 | 254 | Size (bytes) | Description 255 | -------------|------------ 256 | 4 | Entries (pairs) count 257 | 2 or 4 | Kerning pair 1 (glyph id left, glyph id right) 258 | 2 or 4 | Kerning pair 2 259 | ...|... 260 | 2 or 4 | Kerning pair last 261 | 1 | Value 1 262 | 1 | Value 2 263 | ... | ... 264 | 1 | Value last 265 | 266 | Kerning pair size depends on `glyphIdFormat` from header. 267 | 268 | ### Format 3 (Array M*N of classes) 269 | 270 | See Apple's [truetype reference](https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html). 271 | 272 | Data format is very similar to one, suggested by Apple. The only difference is, 273 | we store kerning values directly (without index), because values are 274 | always 1 byte. 275 | 276 | Content: 277 | 278 | Size (bytes) | Description 279 | -------------|------------ 280 | 2 | The number of glyphs in this font (class mapping length). `M`. 281 | 1 | The number of left-hand classes (table rows wigth). `W`. 282 | 1 | The number of right-hand classes (table column height). `H`. 283 | M | left-hand classes mapping, index = glyph_id, value => class id. 284 | M | right-hand classes mapping. 285 | W*H| kerning values array. 286 | 287 | Note about class mappings. Class id `0` is reserved and means "kerning not 288 | exists" for this glyph. It's NOT stored in kerning array. 289 | 290 | Rsulting data is: `kerningArray[(leftClass-1)*rightClassesCount + (rightClass-1)]` 291 | 292 | As been said in original spec, this format is restricted by 256 classes. But 293 | that's enough for very complex cases. For full `Roboto Regular` font dump, 294 | size of auto-restored table is ~80*100 (under auto-restore we mean reverse-build 295 | process, because initially kerning data is extracted for "pairs" without regard 296 | to structures behind). For this reason `Format 2` support was not added to this 297 | spec. 298 | 299 | 300 | ## Compression 301 | 302 | Glyph data uses modified RLE compression - [I3BN](https://thesai.org/Downloads/Volume7No7/Paper_34-New_modified_RLE_algorithms.pdf), with prefilter and tuned options. 303 | 304 | Everything works with "pixels" (groups of 2, 3, 4 or 8 bits). That will not work 305 | for bitonal fonts, but those are small enough. 306 | 307 | Notable compression gain starts with ~30px sizes. That's explained by 2 moments: 308 | 309 | - Kerning table occupies fixed size, notable for small fonts. 310 | - We already "compress" input - store bounding box data only. 311 | 312 | But since decompression costs nothing, we keep it on always, except 1-bpp fonts. 313 | 314 | ### Pre-filter 315 | 316 | Prior to compress data, lines are XOR-ed with previous ones. That gives 10% 317 | of additional gain. Since we don't use advanced entropy encoding, XOR will be 318 | enough and more effective than diff: 319 | 320 | - XOR does not depend on pixel resolution/encoding. 321 | - Can be optimized with word-wide operations if needed. 322 | 323 | ### Compression algorithm 324 | 325 | 1. At least 1 repeat requires to fall into RLE mode (first pixel pass as is). 326 | That helps to pass anti-aliasing pixels without size increase. 327 | 2. First 10 repeats are replaced with `1` bit (`0` - end). 328 | 3. Next (11-th) is followed by 6-bit counter. If counter overflows, max possible 329 | value used, and process starts from beginning (1). 330 | 331 | See [I3BN](https://thesai.org/Downloads/Volume7No7/Paper_34-New_modified_RLE_algorithms.pdf) 332 | for initial idea and images. 333 | 334 | Constants (repeats & counter size) are found by experimenting with different 335 | font sizes & bpp. Those don't affect result too much (only 1-2%). 336 | 337 | Also see: 338 | 339 | - [Compressor sources](https://github.com/lvgl/lv_font_conv/blob/master/lib/font/compress.js). 340 | - [Compressor tests](https://github.com/lvgl/lv_font_conv/blob/master/test/font/test_compress.js). 341 | 342 | ### Decompression 343 | 344 | Everything done in reverse to compression. Data length determined by bitmap size. 345 | 346 | To improve performance, you may apply this "hacks" on decompress stage: 347 | 348 | 1. Align lines in decompressed images to 1 or 4 bytes, to simplify next 349 | operations. For example, post-filter's lines XOR can be done by words. 350 | 2. Return 3-bpp pixels as 4-bpp or 8-bpp, to simplify compose. 351 | -------------------------------------------------------------------------------- /lib/app_error.js: -------------------------------------------------------------------------------- 1 | // Custom Error type to simplify error messaging 2 | // 3 | 'use strict'; 4 | 5 | 6 | //const ExtendableError = require('es6-error'); 7 | //module.exports = class AppError extends ExtendableError {}; 8 | 9 | module.exports = require('make-error')('AppError'); 10 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | // Parse input arguments and execute convertor 2 | 3 | 'use strict'; 4 | 5 | 6 | const argparse = require('argparse'); 7 | const fs = require('fs'); 8 | const mkdirp = require('mkdirp'); 9 | const path = require('path'); 10 | const convert = require('./convert'); 11 | 12 | 13 | class ActionFontAdd extends argparse.Action { 14 | call(parser, namespace, value/*, option_string*/) { 15 | let items = (namespace[this.dest] || []).slice(); 16 | items.push({ source_path: value, ranges: [] }); 17 | namespace[this.dest] = items; 18 | } 19 | } 20 | 21 | 22 | // add range or symbols to font; 23 | // need to merge them into one array here so overrides work correctly 24 | class ActionFontRangeAdd extends argparse.Action { 25 | call(parser, namespace, value, option_string) { 26 | let fonts = namespace.font || []; 27 | 28 | if (fonts.length === 0) { 29 | parser.error(`argument ${option_string}: Only allowed after --font`); 30 | } 31 | 32 | let lastFont = fonts[fonts.length - 1]; 33 | 34 | // { symbols: 'ABC' }, or { range: [ 65, 67, 65 ] } 35 | lastFont.ranges.push({ [this.dest]: value }); 36 | } 37 | } 38 | 39 | 40 | // add hinting option to font; 41 | class ActionFontStoreTrue extends argparse.Action { 42 | constructor(options) { 43 | options = options || {}; 44 | options.const = true; 45 | options.default = options.default !== null ? options.default : false; 46 | options.nargs = 0; 47 | super(options); 48 | } 49 | 50 | call(parser, namespace, value, option_string) { 51 | let fonts = namespace.font || []; 52 | 53 | if (fonts.length === 0) { 54 | parser.error(`argument ${option_string}: Only allowed after --font`); 55 | } 56 | 57 | let lastFont = fonts[fonts.length - 1]; 58 | 59 | lastFont[this.dest] = this.const; 60 | } 61 | } 62 | 63 | 64 | // Formatter with support of `\n` in Help texts. 65 | class RawTextHelpFormatter2 extends argparse.RawDescriptionHelpFormatter { 66 | // executes parent _split_lines for each line of the help, then flattens the result 67 | _split_lines(text, width) { 68 | return [].concat(...text.split('\n').map(line => super._split_lines(line, width))); 69 | } 70 | } 71 | 72 | 73 | // parse decimal or hex code in unicode range 74 | function unicode_point(str) { 75 | let m = /^(?:(?:0x([0-9a-f]+))|([0-9]+))$/i.exec(str.trim()); 76 | 77 | if (!m) throw new TypeError(`${str} is not a number`); 78 | 79 | let [ , hex, dec ] = m; 80 | 81 | let value = hex ? parseInt(hex, 16) : parseInt(dec, 10); 82 | 83 | if (value > 0x10FFFF) throw new TypeError(`${str} is out of unicode range`); 84 | 85 | return value; 86 | } 87 | 88 | 89 | // parse range 90 | function range(str) { 91 | let result = []; 92 | 93 | for (let s of str.split(',')) { 94 | let m = /^(.+?)(?:-(.+?))?(?:=>(.+?))?$/i.exec(s); 95 | 96 | let [ , start, end, mapped_start ] = m; 97 | 98 | if (!end) end = start; 99 | if (!mapped_start) mapped_start = start; 100 | 101 | start = unicode_point(start); 102 | end = unicode_point(end); 103 | 104 | if (start > end) throw new TypeError(`Invalid range: ${s}`); 105 | 106 | mapped_start = unicode_point(mapped_start); 107 | 108 | result.push(start, end, mapped_start); 109 | } 110 | 111 | return result; 112 | } 113 | 114 | 115 | // exclude negative numbers and non-numbers 116 | function positive_int(str) { 117 | if (!/^\d+$/.test(str)) throw new TypeError(`${str} is not a valid number`); 118 | 119 | let n = parseInt(str, 10); 120 | 121 | if (n <= 0) throw new TypeError(`${str} is not a valid number`); 122 | 123 | return n; 124 | } 125 | 126 | 127 | module.exports.run = async function (argv, debug = false) { 128 | 129 | // 130 | // Configure CLI 131 | // 132 | 133 | let parser = new argparse.ArgumentParser({ 134 | add_help: true, 135 | formatter_class: RawTextHelpFormatter2 136 | }); 137 | 138 | if (debug) { 139 | parser.exit = function (status, message) { 140 | throw new Error(message); 141 | }; 142 | } 143 | 144 | parser.add_argument('-v', '--version', { 145 | action: 'version', 146 | version: require('../package.json').version 147 | }); 148 | 149 | parser.add_argument('--size', { 150 | metavar: 'PIXELS', 151 | type: positive_int, 152 | required: true, 153 | help: 'Output font size, pixels.' 154 | }); 155 | 156 | parser.add_argument('-o', '--output', { 157 | metavar: '', 158 | help: 'Output path.' 159 | }); 160 | 161 | parser.add_argument('--bpp', { 162 | choices: [ 1, 2, 3, 4, 8 ], 163 | type: positive_int, 164 | required: true, 165 | help: 'Bits per pixel, for antialiasing.' 166 | }); 167 | 168 | let lcd_group = parser.add_mutually_exclusive_group(); 169 | 170 | lcd_group.add_argument('--lcd', { 171 | action: 'store_true', 172 | default: false, 173 | help: 'Enable subpixel rendering (horizontal pixel layout).' 174 | }); 175 | 176 | lcd_group.add_argument('--lcd-v', { 177 | action: 'store_true', 178 | default: false, 179 | help: 'Enable subpixel rendering (vertical pixel layout).' 180 | }); 181 | 182 | parser.add_argument('--use-color-info', { 183 | dest: 'use_color_info', 184 | action: 'store_true', 185 | default: false, 186 | help: 'Try to use glyph color info from font to create grayscale icons. ' + 187 | 'Since gray tones are emulated via transparency, result will be good on contrast background only.' 188 | }); 189 | 190 | parser.add_argument('--format', { 191 | choices: convert.formats, 192 | required: true, 193 | help: 'Output format.' 194 | }); 195 | 196 | parser.add_argument('--font', { 197 | metavar: '', 198 | action: ActionFontAdd, 199 | required: true, 200 | help: 'Source font path. Can be used multiple times to merge glyphs from different fonts.' 201 | }); 202 | 203 | parser.add_argument('-r', '--range', { 204 | type: range, 205 | action: ActionFontRangeAdd, 206 | help: ` 207 | Range of glyphs to copy. Can be used multiple times, belongs to previously declared "--font". Examples: 208 | -r 0x1F450 209 | -r 0x20-0x7F 210 | -r 32-127 211 | -r 32-127,0x1F450 212 | -r '0x1F450=>0xF005' 213 | -r '0x1F450-0x1F470=>0xF005' 214 | ` 215 | }); 216 | 217 | parser.add_argument('--symbols', { 218 | action: ActionFontRangeAdd, 219 | help: ` 220 | List of characters to copy, belongs to previously declared "--font". Examples: 221 | --symbols ,.0123456789 222 | --symbols abcdefghigklmnopqrstuvwxyz 223 | ` 224 | }); 225 | 226 | parser.add_argument('--autohint-off', { 227 | type: range, 228 | action: ActionFontStoreTrue, 229 | help: 'Disable autohinting for previously declared "--font"' 230 | }); 231 | 232 | parser.add_argument('--autohint-strong', { 233 | type: range, 234 | action: ActionFontStoreTrue, 235 | help: 'Use more strong autohinting for previously declared "--font" (will break kerning)' 236 | }); 237 | 238 | parser.add_argument('--force-fast-kern-format', { 239 | dest: 'fast_kerning', 240 | action: 'store_true', 241 | default: false, 242 | help: 'Always use kern classes instead of pairs (might be larger but faster).' 243 | }); 244 | 245 | parser.add_argument('--no-compress', { 246 | dest: 'no_compress', 247 | action: 'store_true', 248 | default: false, 249 | help: 'Disable built-in RLE compression.' 250 | }); 251 | 252 | parser.add_argument('--no-prefilter', { 253 | dest: 'no_prefilter', 254 | action: 'store_true', 255 | default: false, 256 | help: 'Disable bitmap lines filter (XOR), used to improve compression ratio.' 257 | }); 258 | 259 | parser.add_argument('--no-kerning', { 260 | dest: 'no_kerning', 261 | action: 'store_true', 262 | default: false, 263 | help: 'Drop kerning info to reduce size (not recommended).' 264 | }); 265 | 266 | parser.add_argument('--byte-align', { 267 | dest: 'byte_align', 268 | action: 'store_true', 269 | default: false, 270 | help: 'Pad bitmap line endings to whole bytes.' 271 | }); 272 | 273 | parser.add_argument('--stride', { 274 | choices: [ 0, 1, 4, 8, 16, 32, 64 ], 275 | type: positive_int, 276 | default: 1, 277 | help: 'Align each glyph\'s stride to the specfied number of bytes.' 278 | }); 279 | 280 | parser.add_argument('--align', { 281 | choices: [ 1, 4, 8, 16, 32, 64 ], 282 | type: positive_int, 283 | default: 1, 284 | help: 'Align each glyph address to the specified number of bytes.' 285 | }); 286 | 287 | parser.add_argument('--lv-include', { 288 | metavar: '', 289 | help: 'Set alternate "lvgl.h" path (for --format lvgl).' 290 | }); 291 | 292 | parser.add_argument('--lv-font-name', { 293 | help: 'Variable name of the lvgl font structure. Defaults to the output\'s basename.' 294 | }); 295 | 296 | parser.add_argument('--full-info', { 297 | dest: 'full_info', 298 | action: 'store_true', 299 | default: false, 300 | help: 'Don\'t shorten "font_info.json" (include pixels data).' 301 | }); 302 | 303 | parser.add_argument('--lv-fallback', { 304 | help: 'Variable name of the lvgl font structure to use as fallback for this font. Defaults to NULL.' 305 | }); 306 | 307 | // 308 | // Process CLI options 309 | // 310 | 311 | let args = parser.parse_args(argv.length ? argv : [ '-h' ]); 312 | args.opts_string = process.argv.slice(2).join(' '); 313 | 314 | for (let font of args.font) { 315 | if (font.ranges.length === 0) { 316 | parser.error(`You need to specify either "--range" or "--symbols" for font "${font.source_path}"`); 317 | } 318 | 319 | try { 320 | font.source_bin = fs.readFileSync(font.source_path); 321 | } catch (err) { 322 | parser.error(`Cannot read file "${font.source_path}": ${err.message}`); 323 | } 324 | } 325 | 326 | if (args.byte_align) { 327 | if (args.no_compress === false) { 328 | parser.error('--byte_align requires --no-compress'); 329 | } 330 | 331 | if (args.bpp === 3) { 332 | parser.error('--byte_align requires --bpp 1, 2, 4, or 8'); 333 | } 334 | } 335 | 336 | // 337 | // Convert 338 | // 339 | 340 | let files = await convert(args); 341 | 342 | // 343 | // Store files 344 | // 345 | 346 | for (let [ filename, data ] of Object.entries(files)) { 347 | let dir = path.dirname(filename); 348 | 349 | mkdirp.sync(dir); 350 | 351 | fs.writeFileSync(filename, data); 352 | } 353 | 354 | }; 355 | 356 | 357 | // export for tests 358 | module.exports._range = range; 359 | -------------------------------------------------------------------------------- /lib/collect_font_data.js: -------------------------------------------------------------------------------- 1 | // Read fonts 2 | 3 | 'use strict'; 4 | 5 | 6 | const opentype = require('opentype.js'); 7 | const ft_render = require('./freetype'); 8 | const AppError = require('./app_error'); 9 | const Ranger = require('./ranger'); 10 | 11 | 12 | module.exports = async function collect_font_data(args) { 13 | await ft_render.init(); 14 | 15 | // Duplicate font options as k/v for quick access 16 | let fonts_options = {}; 17 | args.font.forEach(f => { fonts_options[f.source_path] = f; }); 18 | 19 | // read fonts 20 | let fonts_opentype = {}; 21 | let fonts_freetype = {}; 22 | 23 | for (let { source_path, source_bin } of args.font) { 24 | // don't load font again if it's specified multiple times in args 25 | if (fonts_opentype[source_path]) continue; 26 | 27 | try { 28 | let b = source_bin; 29 | 30 | if (Buffer.isBuffer(b)) { 31 | // node.js Buffer -> ArrayBuffer 32 | b = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength); 33 | } 34 | 35 | fonts_opentype[source_path] = opentype.parse(b); 36 | } catch (err) { 37 | throw new AppError(`Cannot load font "${source_path}": ${err.message}`); 38 | } 39 | 40 | fonts_freetype[source_path] = ft_render.fontface_create(source_bin, args.size); 41 | } 42 | 43 | // merge all ranges 44 | let ranger = new Ranger(); 45 | 46 | for (let { source_path, ranges } of args.font) { 47 | let font = fonts_freetype[source_path]; 48 | 49 | for (let item of ranges) { 50 | /* eslint-disable max-depth */ 51 | if (item.range) { 52 | for (let i = 0; i < item.range.length; i += 3) { 53 | let range = item.range.slice(i, i + 3); 54 | let chars = ranger.add_range(source_path, ...range); 55 | let is_empty = true; 56 | 57 | for (let code of chars) { 58 | if (ft_render.glyph_exists(font, code)) { 59 | is_empty = false; 60 | break; 61 | } 62 | } 63 | 64 | if (is_empty) { 65 | let a = '0x' + range[0].toString(16); 66 | let b = '0x' + range[1].toString(16); 67 | throw new AppError(`Font "${source_path}" doesn't have any characters included in range ${a}-${b}`); 68 | } 69 | } 70 | } 71 | 72 | if (item.symbols) { 73 | let chars = ranger.add_symbols(source_path, item.symbols); 74 | let is_empty = true; 75 | 76 | for (let code of chars) { 77 | if (ft_render.glyph_exists(font, code)) { 78 | is_empty = false; 79 | break; 80 | } 81 | } 82 | 83 | if (is_empty) { 84 | throw new AppError(`Font "${source_path}" doesn't have any characters included in "${item.symbols}"`); 85 | } 86 | } 87 | } 88 | } 89 | 90 | let mapping = ranger.get(); 91 | let glyphs = []; 92 | let all_dst_charcodes = Object.keys(mapping).sort((a, b) => a - b).map(Number); 93 | 94 | for (let dst_code of all_dst_charcodes) { 95 | let src_code = mapping[dst_code].code; 96 | let src_font = mapping[dst_code].font; 97 | 98 | if (!ft_render.glyph_exists(fonts_freetype[src_font], src_code)) continue; 99 | 100 | let ft_result = ft_render.glyph_render( 101 | fonts_freetype[src_font], 102 | src_code, 103 | { 104 | autohint_off: fonts_options[src_font].autohint_off, 105 | autohint_strong: fonts_options[src_font].autohint_strong, 106 | lcd: args.lcd, 107 | lcd_v: args.lcd_v, 108 | mono: !args.lcd && !args.lcd_v && args.bpp === 1, 109 | use_color_info: args.use_color_info 110 | } 111 | ); 112 | 113 | glyphs.push({ 114 | code: dst_code, 115 | advanceWidth: ft_result.advance_x, 116 | bbox: { 117 | x: ft_result.x, 118 | y: ft_result.y - ft_result.height, 119 | width: ft_result.width, 120 | height: ft_result.height 121 | }, 122 | kerning: {}, 123 | freetype: ft_result.freetype, 124 | pixels: ft_result.pixels 125 | }); 126 | } 127 | 128 | if (!args.no_kerning) { 129 | let existing_dst_charcodes = glyphs.map(g => g.code); 130 | 131 | for (let { code, kerning } of glyphs) { 132 | let src_code = mapping[code].code; 133 | let src_font = mapping[code].font; 134 | let font = fonts_opentype[src_font]; 135 | let glyph = font.charToGlyph(String.fromCodePoint(src_code)); 136 | 137 | for (let dst_code2 of existing_dst_charcodes) { 138 | // can't merge kerning values from 2 different fonts 139 | if (mapping[dst_code2].font !== src_font) continue; 140 | 141 | let src_code2 = mapping[dst_code2].code; 142 | let glyph2 = font.charToGlyph(String.fromCodePoint(src_code2)); 143 | let krn_value = font.getKerningValue(glyph, glyph2); 144 | 145 | if (krn_value) kerning[dst_code2] = krn_value * args.size / font.unitsPerEm; 146 | 147 | //let krn_value = ft_render.get_kerning(font, src_code, src_code2).x; 148 | //if (krn_value) kerning[dst_code2] = krn_value; 149 | } 150 | } 151 | } 152 | 153 | let first_font = fonts_freetype[args.font[0].source_path]; 154 | let first_font_scale = args.size / first_font.units_per_em; 155 | let os2_metrics = ft_render.fontface_os2_table(first_font); 156 | let post_table = fonts_opentype[args.font[0].source_path].tables.post; 157 | 158 | for (let font of Object.values(fonts_freetype)) ft_render.fontface_destroy(font); 159 | 160 | ft_render.destroy(); 161 | 162 | return { 163 | ascent: Math.max(...glyphs.map(g => g.bbox.y + g.bbox.height)), 164 | descent: Math.min(...glyphs.map(g => g.bbox.y)), 165 | typoAscent: Math.round(os2_metrics.typoAscent * first_font_scale), 166 | typoDescent: Math.round(os2_metrics.typoDescent * first_font_scale), 167 | typoLineGap: Math.round(os2_metrics.typoLineGap * first_font_scale), 168 | size: args.size, 169 | glyphs, 170 | underlinePosition: Math.round(post_table.underlinePosition * first_font_scale), 171 | underlineThickness: Math.round(post_table.underlineThickness * first_font_scale) 172 | }; 173 | }; 174 | -------------------------------------------------------------------------------- /lib/convert.js: -------------------------------------------------------------------------------- 1 | // Internal API to convert input data into output font data 2 | // Used by both CLI and Web wrappers. 3 | 'use strict'; 4 | 5 | const collect_font_data = require('./collect_font_data'); 6 | 7 | let writers = { 8 | dump: require('./writers/dump'), 9 | bin: require('./writers/bin'), 10 | lvgl: require('./writers/lvgl') 11 | }; 12 | 13 | 14 | // 15 | // Input: 16 | // - args like from CLI (optionally extended with binary content of files) 17 | // 18 | // Output: 19 | // - { name1: bin_data1, name2: bin_data2, ... } 20 | // 21 | // returns hash with files to write 22 | // 23 | module.exports = async function convert(args) { 24 | let font_data = await collect_font_data(args); 25 | let files = writers[args.format](args, font_data); 26 | 27 | return files; 28 | }; 29 | 30 | module.exports.formats = Object.keys(writers); 31 | -------------------------------------------------------------------------------- /lib/font/cmap_build_subtables.js: -------------------------------------------------------------------------------- 1 | // Find an optimal configuration of cmap tables representing set of codepoints, 2 | // using simple breadth-first algorithm 3 | // 4 | // Assume that: 5 | // - codepoints have one-to-one correspondence to glyph ids 6 | // - glyph ids are always bigger for bigger codepoints 7 | // - glyph ids are always consecutive (1..N without gaps) 8 | // 9 | // This way we can omit glyph ids from all calculations entirely: if codepoints 10 | // fit in format0, then glyph ids also will. 11 | // 12 | // format6 is not considered, because if glyph ids can be delta-coded, 13 | // multiple format0 tables are guaranteed to be smaller than a single format6. 14 | // 15 | // sparse format is not used because as long as glyph ids are consecutive, 16 | // sparse_tiny will always be preferred. 17 | // 18 | 19 | 'use strict'; 20 | 21 | 22 | function estimate_format0_tiny_size(/*start_code, end_code*/) { 23 | return 16; 24 | } 25 | 26 | function estimate_format0_size(start_code, end_code) { 27 | return 16 + (end_code - start_code + 1); 28 | } 29 | 30 | //function estimate_sparse_size(count) { 31 | // return 16 + count * 4; 32 | //} 33 | 34 | function estimate_sparse_tiny_size(count) { 35 | return 16 + count * 2; 36 | } 37 | 38 | module.exports = function cmap_split(all_codepoints) { 39 | all_codepoints = all_codepoints.sort((a, b) => a - b); 40 | 41 | let min_paths = []; 42 | 43 | for (let i = 0; i < all_codepoints.length; i++) { 44 | let min = { dist: Infinity }; 45 | 46 | for (let j = 0; j <= i; j++) { 47 | let prev_dist = (j - 1 >= 0) ? min_paths[j - 1].dist : 0; 48 | let s; 49 | 50 | if (all_codepoints[i] - all_codepoints[j] < 256) { 51 | s = estimate_format0_size(all_codepoints[j], all_codepoints[i]); 52 | 53 | /* eslint-disable max-depth */ 54 | if (prev_dist + s < min.dist) { 55 | min = { 56 | dist: prev_dist + s, 57 | start: j, 58 | end: i, 59 | format: 'format0' 60 | }; 61 | } 62 | } 63 | 64 | if (all_codepoints[i] - all_codepoints[j] < 256 && all_codepoints[i] - i === all_codepoints[j] - j) { 65 | s = estimate_format0_tiny_size(all_codepoints[j], all_codepoints[i]); 66 | 67 | /* eslint-disable max-depth */ 68 | if (prev_dist + s < min.dist) { 69 | min = { 70 | dist: prev_dist + s, 71 | start: j, 72 | end: i, 73 | format: 'format0_tiny' 74 | }; 75 | } 76 | } 77 | 78 | // tiny sparse will always be preferred over full sparse because glyph ids are consecutive 79 | if (all_codepoints[i] - all_codepoints[j] < 65536) { 80 | s = estimate_sparse_tiny_size(i - j + 1); 81 | 82 | if (prev_dist + s < min.dist) { 83 | min = { 84 | dist: prev_dist + s, 85 | start: j, 86 | end: i, 87 | format: 'sparse_tiny' 88 | }; 89 | } 90 | } 91 | } 92 | 93 | min_paths[i] = min; 94 | } 95 | 96 | let result = []; 97 | 98 | for (let i = all_codepoints.length - 1; i >= 0;) { 99 | let path = min_paths[i]; 100 | result.unshift([ path.format, all_codepoints.slice(path.start, path.end + 1) ]); 101 | i = path.start - 1; 102 | } 103 | 104 | return result; 105 | }; 106 | -------------------------------------------------------------------------------- /lib/font/compress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //const debug = require('debug')('compress'); 4 | 5 | function count_same(arr, offset) { 6 | let same = 1; 7 | let val = arr[offset]; 8 | 9 | for (let i = offset + 1; i < arr.length; i++) { 10 | if (arr[i] !== val) break; 11 | same++; 12 | } 13 | 14 | return same; 15 | } 16 | 17 | // 18 | // Compress pixels with RLE-like algorithm (modified I3BN) 19 | // 20 | // 1. Require minimal repeat count (1) to enter I3BN mode 21 | // 2. Increased 1-bit-replaced repeat limit (2 => 10) 22 | // 3. Length of direct repetition counter reduced (8 => 6 bits). 23 | // 24 | // pixels - flat array of pixels (one per entry) 25 | // options.bpp - bits per pixels 26 | // 27 | module.exports = function compress(bitStream, pixels, options) { 28 | const opts = Object.assign({}, { repeat: 1 }, options); 29 | 30 | // Minimal repetitions count to enable RLE mode. 31 | const RLE_SKIP_COUNT = 1; 32 | // Number of repeats, when `1` used to replace data 33 | // If more - write as number 34 | const RLE_BIT_COLLAPSED_COUNT = 10; 35 | 36 | const RLE_COUNTER_BITS = 6; // (2^bits - 1) - max value 37 | const RLE_COUNTER_MAX = (1 << RLE_COUNTER_BITS) - 1; 38 | // Force flush if counter density exceeded. 39 | const RLE_MAX_REPEATS = RLE_COUNTER_MAX + RLE_BIT_COLLAPSED_COUNT + 1; 40 | 41 | //let bits_start_offset = bitStream.index; 42 | 43 | let offset = 0; 44 | 45 | while (offset < pixels.length) { 46 | const p = pixels[offset]; 47 | 48 | let same = count_same(pixels, offset); 49 | 50 | // Clamp value because RLE counter density is limited 51 | if (same > RLE_MAX_REPEATS + RLE_SKIP_COUNT) { 52 | same = RLE_MAX_REPEATS + RLE_SKIP_COUNT; 53 | } 54 | 55 | //debug(`offset: ${offset}, count: ${same}, pixel: ${p}`); 56 | 57 | offset += same; 58 | 59 | // If not enough for RLE - write as is. 60 | if (same <= RLE_SKIP_COUNT) { 61 | for (let i = 0; i < same; i++) { 62 | bitStream.writeBits(p, opts.bpp); 63 | //debug(`==> ${opts.bpp} bits`); 64 | } 65 | continue; 66 | } 67 | 68 | // First, write "skipped" head as is. 69 | for (let i = 0; i < RLE_SKIP_COUNT; i++) { 70 | bitStream.writeBits(p, opts.bpp); 71 | //debug(`==> ${opts.bpp} bits`); 72 | } 73 | 74 | same -= RLE_SKIP_COUNT; 75 | 76 | // Not reached state to use counter => dump bit-extended 77 | if (same <= RLE_BIT_COLLAPSED_COUNT) { 78 | bitStream.writeBits(p, opts.bpp); 79 | //debug(`==> ${opts.bpp} bits (val)`); 80 | for (let i = 0; i < same; i++) { 81 | /*eslint-disable max-depth*/ 82 | if (i < same - 1) { 83 | bitStream.writeBits(1, 1); 84 | //debug('==> 1 bit (rle repeat)'); 85 | } else { 86 | bitStream.writeBits(0, 1); 87 | //debug('==> 1 bit (rle repeat last)'); 88 | } 89 | } 90 | continue; 91 | } 92 | 93 | same -= RLE_BIT_COLLAPSED_COUNT + 1; 94 | 95 | bitStream.writeBits(p, opts.bpp); 96 | //debug(`==> ${opts.bpp} bits (val)`); 97 | 98 | for (let i = 0; i < RLE_BIT_COLLAPSED_COUNT + 1; i++) { 99 | bitStream.writeBits(1, 1); 100 | //debug('==> 1 bit (rle repeat)'); 101 | } 102 | bitStream.writeBits(same, RLE_COUNTER_BITS); 103 | //debug(`==> 4 bits (rle repeat count ${same})`); 104 | } 105 | 106 | //debug(`output bits: ${bitStream.index - bits_start_offset}`); 107 | }; 108 | -------------------------------------------------------------------------------- /lib/font/font.js: -------------------------------------------------------------------------------- 1 | // Font class to generate tables 2 | 'use strict'; 3 | 4 | const u = require('../utils'); 5 | const debug = require('debug')('font'); 6 | const Head = require('./table_head'); 7 | const Cmap = require('./table_cmap'); 8 | const Glyf = require('./table_glyf'); 9 | const Loca = require('./table_loca'); 10 | const Kern = require('./table_kern'); 11 | 12 | class Font { 13 | constructor(fontData, options) { 14 | this.src = fontData; 15 | 16 | this.opts = options; 17 | 18 | // Map chars to IDs (zero is reserved) 19 | this.glyph_id = { 0: 0 }; 20 | 21 | this.last_id = 1; 22 | this.createIDs(); 23 | debug(`last_id: ${this.last_id}`); 24 | 25 | this.init_tables(); 26 | 27 | this.minY = Math.min(...this.src.glyphs.map(g => g.bbox.y)); 28 | debug(`minY: ${this.minY}`); 29 | this.maxY = Math.max(...this.src.glyphs.map(g => g.bbox.y + g.bbox.height)); 30 | debug(`maxY: ${this.maxY}`); 31 | 32 | // 0 => 1 byte, 1 => 2 bytes 33 | this.glyphIdFormat = Math.max(...Object.values(this.glyph_id)) > 255 ? 1 : 0; 34 | debug(`glyphIdFormat: ${this.glyphIdFormat}`); 35 | 36 | // 1.0 by default, will be stored in font as FP12.4 37 | this.kerningScale = 1.0; 38 | let kerningMax = Math.max(...this.src.glyphs.map(g => Object.values(g.kerning).map(Math.abs)).flat()); 39 | if (kerningMax >= 7.5) this.kerningScale = Math.ceil(kerningMax / 7.5 * 16) / 16; 40 | debug(`kerningScale: ${this.kerningScale}`); 41 | 42 | // 0 => int, 1 => FP4 43 | this.advanceWidthFormat = this.hasKerning() ? 1 : 0; 44 | debug(`advanceWidthFormat: ${this.advanceWidthFormat}`); 45 | 46 | this.xy_bits = Math.max(...this.src.glyphs.map(g => Math.max( 47 | u.signed_bits(g.bbox.x), u.signed_bits(g.bbox.y) 48 | ))); 49 | debug(`xy_bits: ${this.xy_bits}`); 50 | 51 | this.wh_bits = Math.max(...this.src.glyphs.map(g => Math.max( 52 | u.unsigned_bits(g.bbox.width), u.unsigned_bits(g.bbox.height) 53 | ))); 54 | debug(`wh_bits: ${this.wh_bits}`); 55 | 56 | this.advanceWidthBits = Math.max(...this.src.glyphs.map( 57 | g => u.signed_bits(this.widthToInt(g.advanceWidth)) 58 | )); 59 | debug(`advanceWidthBits: ${this.advanceWidthBits}`); 60 | 61 | let glyphs = this.src.glyphs; 62 | 63 | this.monospaced = glyphs.every((v, i, arr) => v.advanceWidth === arr[0].advanceWidth); 64 | debug(`monospaced: ${this.monospaced}`); 65 | 66 | // This should stay in the end, because depends on previous variables 67 | // 0 => 2 bytes, 1 => 4 bytes 68 | this.indexToLocFormat = this.glyf.getSize() > 65535 ? 1 : 0; 69 | debug(`indexToLocFormat: ${this.indexToLocFormat}`); 70 | 71 | this.subpixels_mode = options.lcd ? 1 : (options.lcd_v ? 2 : 0); 72 | debug(`subpixels_mode: ${this.subpixels_mode}`); 73 | } 74 | 75 | init_tables() { 76 | this.head = new Head(this); 77 | this.glyf = new Glyf(this); 78 | this.cmap = new Cmap(this); 79 | this.loca = new Loca(this); 80 | this.kern = new Kern(this); 81 | } 82 | 83 | createIDs() { 84 | // Simplified, don't check dupes 85 | this.last_id = 1; 86 | 87 | for (let i = 0; i < this.src.glyphs.length; i++) { 88 | // reserve zero for special cases 89 | this.glyph_id[this.src.glyphs[i].code] = this.last_id; 90 | this.last_id++; 91 | } 92 | } 93 | 94 | hasKerning() { 95 | if (this.opts.no_kerning) return false; 96 | 97 | for (let glyph of this.src.glyphs) { 98 | if (glyph.kerning && Object.keys(glyph.kerning).length) return true; 99 | } 100 | return false; 101 | } 102 | 103 | // Returns integer width, depending on format 104 | widthToInt(val) { 105 | if (this.advanceWidthFormat === 0) return Math.round(val); 106 | 107 | return Math.round(val * 16); 108 | } 109 | 110 | // Convert kerning to FP4.4, useable for writer. Apply `kerningScale`. 111 | kernToFP(val) { 112 | return Math.round(val / this.kerningScale * 16); 113 | } 114 | 115 | toBin() { 116 | const result = Buffer.concat([ 117 | this.head.toBin(), 118 | this.cmap.toBin(), 119 | this.loca.toBin(), 120 | this.glyf.toBin(), 121 | this.kern.toBin() 122 | ]); 123 | 124 | debug(`font size: ${result.length}`); 125 | 126 | return result; 127 | } 128 | } 129 | 130 | 131 | module.exports = Font; 132 | -------------------------------------------------------------------------------- /lib/font/table_cmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const build_subtables = require('./cmap_build_subtables'); 5 | const u = require('../utils'); 6 | const debug = require('debug')('font.table.cmap'); 7 | 8 | 9 | const O_SIZE = 0; 10 | const O_LABEL = O_SIZE + 4; 11 | const O_COUNT = O_LABEL + 4; 12 | 13 | const HEAD_LENGTH = O_COUNT + 4; 14 | 15 | const SUB_FORMAT_0 = 0; 16 | const SUB_FORMAT_0_TINY = 2; 17 | const SUB_FORMAT_SPARSE = 1; 18 | const SUB_FORMAT_SPARSE_TINY = 3; 19 | 20 | 21 | class Cmap { 22 | constructor(font) { 23 | this.font = font; 24 | this.label = 'cmap'; 25 | 26 | this.sub_heads = []; 27 | this.sub_data = []; 28 | 29 | this.compiled = false; 30 | } 31 | 32 | compile() { 33 | if (this.compiled) return; 34 | this.compiled = true; 35 | 36 | const f = this.font; 37 | 38 | let subtables_plan = build_subtables(f.src.glyphs.map(g => g.code)); 39 | 40 | const count_format0 = subtables_plan.filter(s => s[0] === 'format0').length; 41 | const count_sparse = subtables_plan.length - count_format0; 42 | debug(`${subtables_plan.length} subtable(s): ${count_format0} "format 0", ${count_sparse} "sparse"`); 43 | 44 | for (let [ format, codepoints ] of subtables_plan) { 45 | let g = this.glyphByCode(codepoints[0]); 46 | let start_glyph_id = f.glyph_id[g.code]; 47 | let min_code = codepoints[0]; 48 | let max_code = codepoints[codepoints.length - 1]; 49 | let entries_count = max_code - min_code + 1; 50 | let format_code = 0; 51 | 52 | if (format === 'format0_tiny') { 53 | format_code = SUB_FORMAT_0_TINY; 54 | this.sub_data.push(Buffer.alloc(0)); 55 | } else if (format === 'format0') { 56 | format_code = SUB_FORMAT_0; 57 | this.sub_data.push(this.create_format0_data(min_code, max_code, start_glyph_id)); 58 | } else if (format === 'sparse_tiny') { 59 | entries_count = codepoints.length; 60 | format_code = SUB_FORMAT_SPARSE_TINY; 61 | this.sub_data.push(this.create_sparse_tiny_data(codepoints, start_glyph_id)); 62 | } else { // assume format === 'sparse' 63 | entries_count = codepoints.length; 64 | format_code = SUB_FORMAT_SPARSE; 65 | this.sub_data.push(this.create_sparse_data(codepoints, start_glyph_id)); 66 | } 67 | 68 | this.sub_heads.push(this.createSubHeader( 69 | min_code, 70 | max_code - min_code + 1, 71 | start_glyph_id, 72 | entries_count, 73 | format_code 74 | )); 75 | } 76 | 77 | this.subHeaderUpdateAllOffsets(); 78 | } 79 | 80 | createSubHeader(rangeStart, rangeLen, glyphIdOffset, total, type) { 81 | const buf = Buffer.alloc(16); 82 | 83 | // buf.writeUInt32LE(offset, 0); offset unknown at this moment 84 | buf.writeUInt32LE(rangeStart, 4); 85 | buf.writeUInt16LE(rangeLen, 8); 86 | buf.writeUInt16LE(glyphIdOffset, 10); 87 | buf.writeUInt16LE(total, 12); 88 | buf.writeUInt8(type, 14); 89 | 90 | return buf; 91 | } 92 | 93 | subHeaderUpdateOffset(header, val) { 94 | header.writeUInt32LE(val, 0); 95 | } 96 | 97 | subHeaderUpdateAllOffsets() { 98 | for (let i = 0; i < this.sub_heads.length; i++) { 99 | const offset = HEAD_LENGTH + 100 | u.sum(this.sub_heads.map(h => h.length)) + 101 | u.sum(this.sub_data.slice(0, i).map(d => d.length)); 102 | 103 | this.subHeaderUpdateOffset(this.sub_heads[i], offset); 104 | } 105 | } 106 | 107 | glyphByCode(code) { 108 | for (let g of this.font.src.glyphs) { 109 | if (g.code === code) return g; 110 | } 111 | 112 | return null; 113 | } 114 | 115 | 116 | collect_format0_data(min_code, max_code, start_glyph_id) { 117 | let data = []; 118 | 119 | for (let i = min_code; i <= max_code; i++) { 120 | const g = this.glyphByCode(i); 121 | 122 | if (!g) { 123 | data.push(0); 124 | continue; 125 | } 126 | 127 | const id_delta = this.font.glyph_id[g.code] - start_glyph_id; 128 | 129 | if (id_delta < 0 || id_delta > 255) throw new Error('Glyph ID delta out of Format 0 range'); 130 | 131 | data.push(id_delta); 132 | } 133 | 134 | return data; 135 | } 136 | 137 | create_format0_data(min_code, max_code, start_glyph_id) { 138 | const data = this.collect_format0_data(min_code, max_code, start_glyph_id); 139 | 140 | return u.balign4(Buffer.from(data)); 141 | } 142 | 143 | collect_sparse_data(codepoints, start_glyph_id) { 144 | let codepoints_list = []; 145 | let ids_list = []; 146 | 147 | for (let code of codepoints) { 148 | let g = this.glyphByCode(code); 149 | let id = this.font.glyph_id[g.code]; 150 | 151 | let code_delta = code - codepoints[0]; 152 | let id_delta = id - start_glyph_id; 153 | 154 | if (code_delta < 0 || code_delta > 65535) throw new Error('Codepoint delta out of range'); 155 | if (id_delta < 0 || id_delta > 65535) throw new Error('Glyph ID delta out of range'); 156 | 157 | codepoints_list.push(code_delta); 158 | ids_list.push(id_delta); 159 | } 160 | 161 | return { 162 | codes: codepoints_list, 163 | ids: ids_list 164 | }; 165 | } 166 | 167 | create_sparse_data(codepoints, start_glyph_id) { 168 | const data = this.collect_sparse_data(codepoints, start_glyph_id); 169 | 170 | return u.balign4(Buffer.concat([ 171 | u.bFromA16(data.codes), 172 | u.bFromA16(data.ids) 173 | ])); 174 | } 175 | 176 | create_sparse_tiny_data(codepoints, start_glyph_id) { 177 | const data = this.collect_sparse_data(codepoints, start_glyph_id); 178 | 179 | return u.balign4(u.bFromA16(data.codes)); 180 | } 181 | 182 | toBin() { 183 | if (!this.compiled) this.compile(); 184 | 185 | const buf = Buffer.concat([ 186 | Buffer.alloc(HEAD_LENGTH), 187 | Buffer.concat(this.sub_heads), 188 | Buffer.concat(this.sub_data) 189 | ]); 190 | debug(`table size = ${buf.length}`); 191 | 192 | buf.writeUInt32LE(buf.length, O_SIZE); 193 | buf.write(this.label, O_LABEL); 194 | buf.writeUInt32LE(this.sub_heads.length, O_COUNT); 195 | 196 | return buf; 197 | } 198 | } 199 | 200 | 201 | module.exports = Cmap; 202 | -------------------------------------------------------------------------------- /lib/font/table_glyf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const u = require('../utils'); 4 | const { BitStream } = require('bit-buffer'); 5 | const debug = require('debug')('font.table.glyf'); 6 | const compress = require('./compress'); 7 | 8 | 9 | const O_SIZE = 0; 10 | const O_LABEL = O_SIZE + 4; 11 | 12 | const HEAD_LENGTH = O_LABEL + 4; 13 | 14 | 15 | class Glyf { 16 | constructor(font) { 17 | this.font = font; 18 | this.label = 'glyf'; 19 | 20 | this.compiled = false; 21 | 22 | this.binData = []; 23 | } 24 | 25 | // convert 8-bit opacity to bpp-bit 26 | pixelsToBpp(pixels) { 27 | const bpp = this.font.opts.bpp; 28 | return pixels.map(line => line.map(p => (p >>> (8 - bpp)))); 29 | } 30 | 31 | // Returns "binary stream" (Buffer) of compiled glyph data 32 | compileGlyph(glyph) { 33 | // Allocate memory, enough for eny storage formats 34 | let buf; 35 | 36 | if (this.font.opts.align !== 1 || this.font.opts.stride !== 1) { 37 | buf = Buffer.alloc(100 + glyph.bbox.width * glyph.bbox.height * 4 * 4); 38 | } else { 39 | buf = Buffer.alloc(100 + glyph.bbox.width * glyph.bbox.height * 4); 40 | } 41 | 42 | const bs = new BitStream(buf); 43 | bs.bigEndian = true; 44 | const f = this.font; 45 | 46 | // Store Width 47 | if (!f.monospaced) { 48 | let w = f.widthToInt(glyph.advanceWidth); 49 | bs.writeBits(w, f.advanceWidthBits); 50 | } 51 | 52 | // Store X, Y 53 | bs.writeBits(glyph.bbox.x, f.xy_bits); 54 | bs.writeBits(glyph.bbox.y, f.xy_bits); 55 | bs.writeBits(glyph.bbox.width, f.wh_bits); 56 | bs.writeBits(glyph.bbox.height, f.wh_bits); 57 | 58 | const pixels = this.pixelsToBpp(glyph.pixels); 59 | 60 | this.storePixels(bs, pixels); 61 | 62 | // Shrink size 63 | let result; 64 | 65 | if (this.font.opts.align && this.font.opts.align !== 1) { 66 | result = Buffer.alloc(Math.ceil(bs.byteIndex / this.font.opts.align) * this.font.opts.align); 67 | } else { 68 | result = Buffer.alloc(bs.byteIndex); 69 | } 70 | 71 | buf.copy(result, 0, 0, bs.byteIndex); 72 | 73 | return result; 74 | } 75 | 76 | storePixels(bitStream, pixels) { 77 | if (this.getCompressionCode() === 0 || this.getCompressionCode() === 3) this.storePixelsRaw(bitStream, pixels); 78 | else this.storePixelsCompressed(bitStream, pixels); 79 | } 80 | 81 | addPadding(bitStream, pad) { 82 | const bpp = this.font.opts.bpp; 83 | for (let x = 0; x < pad; x++) { 84 | bitStream.writeBits(0, bpp); 85 | } 86 | } 87 | 88 | storePixelsRaw(bitStream, pixels) { 89 | if (pixels.length === 0) return; 90 | 91 | const bpp = this.font.opts.bpp; 92 | let pad = 0; 93 | if (this.font.opts.byte_align) { 94 | const pxPerByte = 8 / bpp; 95 | pad = pxPerByte - (pixels[0].length % pxPerByte); 96 | } 97 | 98 | for (let y = 0; y < pixels.length; y++) { 99 | const line = pixels[y]; 100 | if (this.font.opts.stride && this.font.opts.stride !== 1) { 101 | let stride = this.font.opts.stride; 102 | const alignedLine = Buffer.alloc(Math.ceil(line.length / stride) * stride); 103 | alignedLine.fill(Buffer.from(line), 0, line.length); 104 | for (let x = 0; x < alignedLine.length; x++) { 105 | bitStream.writeBits(alignedLine[x], bpp); 106 | } 107 | } else { 108 | for (let x = 0; x < line.length; x++) { 109 | bitStream.writeBits(line[x], bpp); 110 | } 111 | if (pad) { 112 | this.addPadding(bitStream, pad); 113 | } 114 | } 115 | } 116 | } 117 | 118 | storePixelsCompressed(bitStream, pixels) { 119 | let p; 120 | 121 | if (this.font.opts.no_prefilter) p = pixels.flat(); 122 | else p = u.prefilter(pixels).flat(); 123 | 124 | compress(bitStream, p, this.font.opts); 125 | } 126 | 127 | // Create internal struct with binary data for each glyph 128 | // Needed to calculate offsets & build final result 129 | compile() { 130 | this.compiled = true; 131 | 132 | this.binData = [ 133 | Buffer.alloc(0) // Reserve id 0 134 | ]; 135 | 136 | const f = this.font; 137 | 138 | f.src.glyphs.forEach(g => { 139 | const id = f.glyph_id[g.code]; 140 | 141 | this.binData[id] = this.compileGlyph(g); 142 | }); 143 | } 144 | 145 | toBin() { 146 | if (!this.compiled) this.compile(); 147 | 148 | const buf = u.balign4(Buffer.concat([ 149 | Buffer.alloc(HEAD_LENGTH), 150 | Buffer.concat(this.binData) 151 | ])); 152 | 153 | buf.writeUInt32LE(buf.length, O_SIZE); 154 | buf.write(this.label, O_LABEL); 155 | 156 | debug(`table size = ${buf.length}`); 157 | 158 | return buf; 159 | } 160 | 161 | getSize() { 162 | if (!this.compiled) this.compile(); 163 | 164 | return u.align4(HEAD_LENGTH + u.sum(this.binData.map(b => b.length))); 165 | } 166 | 167 | getOffset(id) { 168 | if (!this.compiled) this.compile(); 169 | 170 | let offset = HEAD_LENGTH; 171 | 172 | for (let i = 0; i < id; i++) offset += this.binData[i].length; 173 | 174 | return offset; 175 | } 176 | 177 | getCompressionCode() { 178 | if (this.font.opts.byte_align) return 3; 179 | if (this.font.opts.no_compress) return 0; 180 | if (this.font.opts.bpp === 1) return 0; 181 | if (this.font.opts.no_prefilter) return 2; 182 | return 1; 183 | } 184 | } 185 | 186 | 187 | module.exports = Glyf; 188 | -------------------------------------------------------------------------------- /lib/font/table_head.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const u = require('../utils'); 5 | const debug = require('debug')('font.table.head'); 6 | 7 | const O_SIZE = 0; 8 | const O_LABEL = O_SIZE + 4; 9 | const O_VERSION = O_LABEL + 4; 10 | const O_TABLES = O_VERSION + 4; 11 | const O_FONT_SIZE = O_TABLES + 2; 12 | const O_ASCENT = O_FONT_SIZE + 2; 13 | const O_DESCENT = O_ASCENT + 2; 14 | const O_TYPO_ASCENT = O_DESCENT + 2; 15 | const O_TYPO_DESCENT = O_TYPO_ASCENT + 2; 16 | const O_TYPO_LINE_GAP = O_TYPO_DESCENT + 2; 17 | const O_MIN_Y = O_TYPO_LINE_GAP + 2; 18 | const O_MAX_Y = O_MIN_Y + 2; 19 | const O_DEF_ADVANCE_WIDTH = O_MAX_Y + 2; 20 | const O_KERNING_SCALE = O_DEF_ADVANCE_WIDTH + 2; 21 | const O_INDEX_TO_LOC_FORMAT = O_KERNING_SCALE + 2; 22 | const O_GLYPH_ID_FORMAT = O_INDEX_TO_LOC_FORMAT + 1; 23 | const O_ADVANCE_WIDTH_FORMAT = O_GLYPH_ID_FORMAT + 1; 24 | const O_BITS_PER_PIXEL = O_ADVANCE_WIDTH_FORMAT + 1; 25 | const O_XY_BITS = O_BITS_PER_PIXEL + 1; 26 | const O_WH_BITS = O_XY_BITS + 1; 27 | const O_ADVANCE_WIDTH_BITS = O_WH_BITS + 1; 28 | const O_COMPRESSION_ID = O_ADVANCE_WIDTH_BITS + 1; 29 | const O_SUBPIXELS_MODE = O_COMPRESSION_ID + 1; 30 | const O_TMP_RESERVED1 = O_SUBPIXELS_MODE + 1; 31 | const O_UNDERLINE_POSITION = O_TMP_RESERVED1 + 1; 32 | const O_UNDERLINE_THICKNESS = O_UNDERLINE_POSITION + 2; 33 | const HEAD_LENGTH = u.align4(O_UNDERLINE_THICKNESS + 2); 34 | 35 | 36 | class Head { 37 | constructor(font) { 38 | this.font = font; 39 | this.label = 'head'; 40 | this.version = 1; 41 | } 42 | 43 | toBin() { 44 | const buf = Buffer.alloc(HEAD_LENGTH); 45 | debug(`table size = ${buf.length}`); 46 | 47 | buf.writeUInt32LE(HEAD_LENGTH, O_SIZE); 48 | buf.write(this.label, O_LABEL); 49 | buf.writeUInt32LE(this.version, O_VERSION); 50 | 51 | const f = this.font; 52 | 53 | const tables_count = f.hasKerning() ? 4 : 3; 54 | 55 | buf.writeUInt16LE(tables_count, O_TABLES); 56 | 57 | buf.writeUInt16LE(f.src.size, O_FONT_SIZE); 58 | buf.writeUInt16LE(f.src.ascent, O_ASCENT); 59 | buf.writeInt16LE(f.src.descent, O_DESCENT); 60 | 61 | buf.writeUInt16LE(f.src.typoAscent, O_TYPO_ASCENT); 62 | buf.writeInt16LE(f.src.typoDescent, O_TYPO_DESCENT); 63 | buf.writeUInt16LE(f.src.typoLineGap, O_TYPO_LINE_GAP); 64 | 65 | buf.writeInt16LE(f.minY, O_MIN_Y); 66 | buf.writeInt16LE(f.maxY, O_MAX_Y); 67 | 68 | if (f.monospaced) { 69 | buf.writeUInt16LE(f.widthToInt(f.src.glyphs[0].advanceWidth), O_DEF_ADVANCE_WIDTH); 70 | } else { 71 | buf.writeUInt16LE(0, O_DEF_ADVANCE_WIDTH); 72 | } 73 | 74 | buf.writeUInt16LE(Math.round(f.kerningScale * 16), O_KERNING_SCALE); // FP12.4 75 | 76 | buf.writeUInt8(f.indexToLocFormat, O_INDEX_TO_LOC_FORMAT); 77 | buf.writeUInt8(f.glyphIdFormat, O_GLYPH_ID_FORMAT); 78 | buf.writeUInt8(f.advanceWidthFormat, O_ADVANCE_WIDTH_FORMAT); 79 | 80 | buf.writeUInt8(f.opts.bpp, O_BITS_PER_PIXEL); 81 | buf.writeUInt8(f.xy_bits, O_XY_BITS); 82 | buf.writeUInt8(f.wh_bits, O_WH_BITS); 83 | 84 | if (f.monospaced) buf.writeUInt8(0, O_ADVANCE_WIDTH_BITS); 85 | else buf.writeUInt8(f.advanceWidthBits, O_ADVANCE_WIDTH_BITS); 86 | 87 | buf.writeUInt8(f.glyf.getCompressionCode(), O_COMPRESSION_ID); 88 | 89 | buf.writeUInt8(f.subpixels_mode, O_SUBPIXELS_MODE); 90 | 91 | buf.writeInt16LE(f.src.underlinePosition, O_UNDERLINE_POSITION); 92 | buf.writeUInt16LE(f.src.underlineThickness, O_UNDERLINE_POSITION); 93 | 94 | return buf; 95 | } 96 | } 97 | 98 | 99 | module.exports = Head; 100 | -------------------------------------------------------------------------------- /lib/font/table_kern.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const u = require('../utils'); 4 | const debug = require('debug')('font.table.kern'); 5 | 6 | 7 | const O_SIZE = 0; 8 | const O_LABEL = O_SIZE + 4; 9 | const O_FORMAT = O_LABEL + 4; 10 | 11 | const HEAD_LENGTH = u.align4(O_FORMAT + 1); 12 | 13 | 14 | class Kern { 15 | constructor(font) { 16 | this.font = font; 17 | this.label = 'kern'; 18 | this.format3_forced = false; 19 | } 20 | 21 | collect_format0_data() { 22 | const f = this.font; 23 | const glyphs = u.sort_by(this.font.src.glyphs, g => f.glyph_id[g.code]); 24 | const kernSorted = []; 25 | 26 | for (let g of glyphs) { 27 | if (!g.kerning || !Object.keys(g.kerning).length) continue; 28 | 29 | const glyph_id = f.glyph_id[g.code]; 30 | const paired = u.sort_by(Object.keys(g.kerning), code => f.glyph_id[code]); 31 | 32 | for (let code of paired) { 33 | const glyph_id2 = f.glyph_id[code]; 34 | kernSorted.push([ glyph_id, glyph_id2, g.kerning[code] ]); 35 | } 36 | } 37 | 38 | return kernSorted; 39 | } 40 | 41 | create_format0_data() { 42 | const f = this.font; 43 | const glyphs = this.font.src.glyphs; 44 | const kernSorted = this.collect_format0_data(); 45 | 46 | const count = kernSorted.length; 47 | 48 | const kerned_glyphs = glyphs.filter(g => Object.keys(g.kerning).length).length; 49 | const kerning_list_max = Math.max(...glyphs.map(g => Object.keys(g.kerning).length)); 50 | debug(`${kerned_glyphs} kerned glyphs of ${glyphs.length}, ${kerning_list_max} max list, ${count} total pairs`); 51 | 52 | const subheader = Buffer.alloc(4); 53 | 54 | subheader.writeUInt32LE(count, 0); 55 | 56 | const pairs_buf = Buffer.alloc((f.glyphIdFormat ? 4 : 2) * count); 57 | 58 | // Write kerning pairs 59 | for (let i = 0; i < count; i++) { 60 | if (f.glyphIdFormat === 0) { 61 | pairs_buf.writeUInt8(kernSorted[i][0], 2 * i); 62 | pairs_buf.writeUInt8(kernSorted[i][1], 2 * i + 1); 63 | } else { 64 | pairs_buf.writeUInt16LE(kernSorted[i][0], 4 * i); 65 | pairs_buf.writeUInt16LE(kernSorted[i][1], 4 * i + 2); 66 | } 67 | } 68 | 69 | const values_buf = Buffer.alloc(count); 70 | 71 | // Write kerning values 72 | for (let i = 0; i < count; i++) { 73 | values_buf.writeInt8(f.kernToFP(kernSorted[i][2]), i); // FP4.4 74 | } 75 | 76 | let buf = Buffer.concat([ 77 | subheader, 78 | pairs_buf, 79 | values_buf 80 | ]); 81 | 82 | let buf_aligned = u.balign4(buf); 83 | 84 | debug(`table format0 size = ${buf_aligned.length}`); 85 | return buf_aligned; 86 | } 87 | 88 | collect_format3_data() { 89 | const f = this.font; 90 | const glyphs = u.sort_by(this.font.src.glyphs, g => f.glyph_id[g.code]); 91 | 92 | // extract kerning pairs for each character; 93 | // left kernings are kerning values based on left char (already there), 94 | // right kernings are kerning values based on right char (extracted from left) 95 | const left_kernings = {}; 96 | const right_kernings = {}; 97 | 98 | for (let g of glyphs) { 99 | if (!g.kerning || !Object.keys(g.kerning).length) continue; 100 | 101 | const paired = Object.keys(g.kerning); 102 | 103 | left_kernings[g.code] = g.kerning; 104 | 105 | for (let code of paired) { 106 | right_kernings[code] = right_kernings[code] || {}; 107 | right_kernings[code][g.code] = g.kerning[code]; 108 | } 109 | } 110 | 111 | // input: 112 | // - kernings, char => { hash: String, [char1]: Number, [char2]: Number, ... } 113 | // 114 | // returns: 115 | // - array of [ char1, char2, ... ] 116 | // 117 | function build_classes(kernings) { 118 | const classes = []; 119 | 120 | for (let code of Object.keys(kernings)) { 121 | // for each kerning table calculate unique value representing it; 122 | // keys needs to be sorted for this (but we're using numeric keys, so 123 | // sorting happens automatically and can't be changed) 124 | const hash = JSON.stringify(kernings[code]); 125 | 126 | classes[hash] = classes[hash] || []; 127 | classes[hash].push(Number(code)); 128 | } 129 | 130 | return Object.values(classes); 131 | } 132 | 133 | const left_classes = build_classes(left_kernings); 134 | debug(`unique left classes: ${left_classes.length}`); 135 | 136 | const right_classes = build_classes(right_kernings); 137 | debug(`unique right classes: ${right_classes.length}`); 138 | 139 | if (left_classes.length >= 255 || right_classes.length >= 255) { 140 | debug('too many classes for format3 subtable'); 141 | return null; 142 | } 143 | 144 | function kern_class_mapping(classes) { 145 | const arr = Array(f.last_id).fill(0); 146 | 147 | classes.forEach((members, idx) => { 148 | for (let code of members) { 149 | arr[f.glyph_id[code]] = idx + 1; 150 | } 151 | }); 152 | 153 | return arr; 154 | } 155 | 156 | function kern_class_values() { 157 | const arr = []; 158 | 159 | for (let left_class of left_classes) { 160 | for (let right_class of right_classes) { 161 | let code1 = left_class[0]; 162 | let code2 = right_class[0]; 163 | arr.push(left_kernings[code1][code2] || 0); 164 | } 165 | } 166 | 167 | return arr; 168 | } 169 | 170 | return { 171 | left_classes: left_classes.length, 172 | right_classes: right_classes.length, 173 | left_mapping: kern_class_mapping(left_classes), 174 | right_mapping: kern_class_mapping(right_classes), 175 | values: kern_class_values() 176 | }; 177 | } 178 | 179 | create_format3_data() { 180 | const f = this.font; 181 | const { 182 | left_classes, 183 | right_classes, 184 | left_mapping, 185 | right_mapping, 186 | values 187 | } = this.collect_format3_data(); 188 | 189 | const subheader = Buffer.alloc(4); 190 | subheader.writeUInt16LE(f.last_id); 191 | subheader.writeUInt8(left_classes, 2); 192 | subheader.writeUInt8(right_classes, 3); 193 | 194 | let buf = Buffer.concat([ 195 | subheader, 196 | Buffer.from(left_mapping), 197 | Buffer.from(right_mapping), 198 | Buffer.from(values.map(v => f.kernToFP(v))) 199 | ]); 200 | 201 | let buf_aligned = u.balign4(buf); 202 | 203 | debug(`table format3 size = ${buf_aligned.length}`); 204 | return buf_aligned; 205 | } 206 | 207 | should_use_format3() { 208 | if (!this.font.hasKerning()) return false; 209 | 210 | const format0_data = this.create_format0_data(); 211 | const format3_data = this.create_format3_data(); 212 | 213 | if (format3_data && format3_data.length <= format0_data.length) return true; 214 | 215 | if (this.font.opts.fast_kerning && format3_data) { 216 | this.format3_forced = true; 217 | return true; 218 | } 219 | 220 | return false; 221 | } 222 | 223 | toBin() { 224 | if (!this.font.hasKerning()) return Buffer.alloc(0); 225 | 226 | const format0_data = this.create_format0_data(); 227 | const format3_data = this.create_format3_data(); 228 | 229 | let header = Buffer.alloc(HEAD_LENGTH); 230 | 231 | let data = format0_data; 232 | header.writeUInt8(0, O_FORMAT); 233 | 234 | /* eslint-disable no-console */ 235 | 236 | if (this.should_use_format3()) { 237 | data = format3_data; 238 | header.writeUInt8(3, O_FORMAT); 239 | 240 | if (this.format3_forced) { 241 | let diff = format3_data.length - format0_data.length; 242 | console.log(`Forced faster kerning format (via classes). Size increase is ${diff} bytes.`); 243 | } 244 | } else if (this.font.opts.fast_kerning) { 245 | console.log('Forced faster kerning format (via classes), but data exceeds it\'s limits. Continue use pairs.'); 246 | } 247 | 248 | header.writeUInt32LE(header.length + data.length, O_SIZE); 249 | header.write(this.label, O_LABEL); 250 | 251 | return Buffer.concat([ header, data ]); 252 | } 253 | } 254 | 255 | 256 | module.exports = Kern; 257 | -------------------------------------------------------------------------------- /lib/font/table_loca.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const u = require('../utils'); 5 | const debug = require('debug')('font.table.loca'); 6 | 7 | 8 | const O_SIZE = 0; 9 | const O_LABEL = O_SIZE + 4; 10 | const O_COUNT = O_LABEL + 4; 11 | 12 | const HEAD_LENGTH = O_COUNT + 4; 13 | 14 | 15 | class Loca { 16 | constructor(font) { 17 | this.font = font; 18 | this.label = 'loca'; 19 | } 20 | 21 | toBin() { 22 | const f = this.font; 23 | 24 | const offsets = [ ...Array(f.last_id).keys() ].map(i => f.glyf.getOffset(i)); 25 | 26 | const buf = u.balign4(Buffer.concat([ 27 | Buffer.alloc(HEAD_LENGTH), 28 | f.indexToLocFormat ? u.bFromA32(offsets) : u.bFromA16(offsets) 29 | ])); 30 | 31 | buf.writeUInt32LE(buf.length, O_SIZE); 32 | buf.write(this.label, O_LABEL); 33 | buf.writeUInt32LE(f.last_id, O_COUNT); 34 | 35 | debug(`table size = ${buf.length}`); 36 | 37 | return buf; 38 | } 39 | } 40 | 41 | 42 | module.exports = Loca; 43 | -------------------------------------------------------------------------------- /lib/freetype/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const ft_render_fabric = require('./build/ft_render'); 5 | 6 | let m = null; // compiled module instance 7 | let library = 0; // pointer to library struct in wasm memory 8 | 9 | 10 | // workaround because of bug in emscripten: 11 | // https://github.com/emscripten-core/emscripten/issues/5820 12 | const runtime_initialized = new Promise(resolve => { 13 | ft_render_fabric().then(module_instance => { 14 | m = module_instance; 15 | resolve(); 16 | }); 17 | }); 18 | 19 | function from_16_16(fixed_point) { 20 | return fixed_point / (1 << 16); 21 | } 22 | 23 | function from_26_6(fixed_point) { 24 | return fixed_point / (1 << 6); 25 | } 26 | 27 | function int8_to_uint8(value) { 28 | return value >= 0 ? value : value + 0x100; 29 | } 30 | 31 | let FT_New_Memory_Face, 32 | FT_Set_Char_Size, 33 | FT_Set_Pixel_Sizes, 34 | FT_Get_Char_Index, 35 | FT_Load_Glyph, 36 | FT_Get_Sfnt_Table, 37 | FT_Get_Kerning, 38 | FT_Done_Face; 39 | 40 | module.exports.init = async function () { 41 | await runtime_initialized; 42 | m._init_constants(); 43 | 44 | FT_New_Memory_Face = module.exports.FT_New_Memory_Face = 45 | m.cwrap('FT_New_Memory_Face', 'number', [ 'number', 'number', 'number', 'number', 'number' ]); 46 | 47 | FT_Set_Char_Size = module.exports.FT_Set_Char_Size = 48 | m.cwrap('FT_Set_Char_Size', 'number', [ 'number', 'number', 'number', 'number', 'number' ]); 49 | 50 | FT_Set_Pixel_Sizes = module.exports.FT_Set_Pixel_Sizes = 51 | m.cwrap('FT_Set_Pixel_Sizes', 'number', [ 'number', 'number', 'number' ]); 52 | 53 | FT_Get_Char_Index = module.exports.FT_Get_Char_Index = 54 | m.cwrap('FT_Get_Char_Index', 'number', [ 'number', 'number' ]); 55 | 56 | FT_Load_Glyph = module.exports.FT_Load_Glyph = 57 | m.cwrap('FT_Load_Glyph', 'number', [ 'number', 'number', 'number' ]); 58 | 59 | FT_Get_Sfnt_Table = module.exports.FT_Get_Sfnt_Table = 60 | m.cwrap('FT_Get_Sfnt_Table', 'number', [ 'number', 'number' ]); 61 | 62 | FT_Get_Kerning = module.exports.FT_Get_Kerning = 63 | m.cwrap('FT_Get_Kerning', 'number', [ 'number', 'number', 'number', 'number', 'number' ]); 64 | 65 | FT_Done_Face = module.exports.FT_Done_Face = 66 | m.cwrap('FT_Done_Face', 'number', [ 'number' ]); 67 | 68 | if (!library) { 69 | let ptr = m._malloc(4); 70 | 71 | try { 72 | let error = m.ccall('FT_Init_FreeType', 'number', [ 'number' ], [ ptr ]); 73 | 74 | if (error) throw new Error(`error in FT_Init_FreeType: ${error}`); 75 | 76 | library = m.getValue(ptr, 'i32'); 77 | } finally { 78 | m._free(ptr); 79 | } 80 | } 81 | }; 82 | 83 | 84 | module.exports.fontface_create = function (source, size) { 85 | let error; 86 | let face = { 87 | ptr: 0, 88 | font: m._malloc(source.length) 89 | }; 90 | 91 | m.writeArrayToMemory(source, face.font); 92 | 93 | let ptr = m._malloc(4); 94 | 95 | try { 96 | error = FT_New_Memory_Face(library, face.font, source.length, 0, ptr); 97 | 98 | if (error) throw new Error(`error in FT_New_Memory_Face: ${error}`); 99 | 100 | face.ptr = m.getValue(ptr, 'i32'); 101 | } finally { 102 | m._free(ptr); 103 | } 104 | 105 | error = FT_Set_Char_Size(face.ptr, 0, size * 64, 300, 300); 106 | 107 | if (error) throw new Error(`error in FT_Set_Char_Size: ${error}`); 108 | 109 | error = FT_Set_Pixel_Sizes(face.ptr, 0, size); 110 | 111 | if (error) throw new Error(`error in FT_Set_Pixel_Sizes: ${error}`); 112 | 113 | let units_per_em = m.getValue(face.ptr + m.OFFSET_FACE_UNITS_PER_EM, 'i16'); 114 | let ascender = m.getValue(face.ptr + m.OFFSET_FACE_ASCENDER, 'i16'); 115 | let descender = m.getValue(face.ptr + m.OFFSET_FACE_DESCENDER, 'i16'); 116 | let height = m.getValue(face.ptr + m.OFFSET_FACE_HEIGHT, 'i16'); 117 | 118 | return Object.assign(face, { 119 | units_per_em, 120 | ascender, 121 | descender, 122 | height 123 | }); 124 | }; 125 | 126 | 127 | module.exports.fontface_os2_table = function (face) { 128 | let sfnt_ptr = FT_Get_Sfnt_Table(face.ptr, m.FT_SFNT_OS2); 129 | 130 | if (!sfnt_ptr) throw new Error('os/2 table not found for this font'); 131 | 132 | let typoAscent = m.getValue(sfnt_ptr + m.OFFSET_TT_OS2_ASCENDER, 'i16'); 133 | let typoDescent = m.getValue(sfnt_ptr + m.OFFSET_TT_OS2_DESCENDER, 'i16'); 134 | let typoLineGap = m.getValue(sfnt_ptr + m.OFFSET_TT_OS2_LINEGAP, 'i16'); 135 | 136 | return { 137 | typoAscent, 138 | typoDescent, 139 | typoLineGap 140 | }; 141 | }; 142 | 143 | 144 | module.exports.get_kerning = function (face, code1, code2) { 145 | let glyph1 = FT_Get_Char_Index(face.ptr, code1); 146 | let glyph2 = FT_Get_Char_Index(face.ptr, code2); 147 | let ptr = m._malloc(4 * 2); 148 | 149 | try { 150 | let error = FT_Get_Kerning(face.ptr, glyph1, glyph2, m.FT_KERNING_DEFAULT, ptr); 151 | 152 | if (error) throw new Error(`error in FT_Get_Kerning: ${error}`); 153 | } finally { 154 | m._free(ptr); 155 | } 156 | 157 | return { 158 | x: from_26_6(m.getValue(ptr, 'i32')), 159 | y: from_26_6(m.getValue(ptr + 4, 'i32')) 160 | }; 161 | }; 162 | 163 | 164 | module.exports.glyph_exists = function (face, code) { 165 | let glyph_index = FT_Get_Char_Index(face.ptr, code); 166 | 167 | return glyph_index !== 0; 168 | }; 169 | 170 | 171 | module.exports.glyph_render = function (face, code, opts = {}) { 172 | let glyph_index = FT_Get_Char_Index(face.ptr, code); 173 | 174 | if (glyph_index === 0) throw new Error(`glyph does not exist for codepoint ${code}`); 175 | 176 | let load_flags = m.FT_LOAD_RENDER; 177 | 178 | if (opts.mono) { 179 | load_flags |= m.FT_LOAD_TARGET_MONO; 180 | 181 | } else if (opts.lcd) { 182 | load_flags |= m.FT_LOAD_TARGET_LCD; 183 | 184 | } else if (opts.lcd_v) { 185 | load_flags |= m.FT_LOAD_TARGET_LCD_V; 186 | 187 | } else { 188 | /* eslint-disable no-lonely-if */ 189 | 190 | // Use "light" by default, it changes horizontal lines only. 191 | // "normal" is more strong (with vertical lines), but will break kerning, if 192 | // no additional care taken. More advanced rendering requires upper level 193 | // layout support (via Harfbuzz, for example). 194 | if (!opts.autohint_strong) load_flags |= m.FT_LOAD_TARGET_LIGHT; 195 | else load_flags |= m.FT_LOAD_TARGET_NORMAL; 196 | } 197 | 198 | if (opts.autohint_off) load_flags |= m.FT_LOAD_NO_AUTOHINT; 199 | else load_flags |= m.FT_LOAD_FORCE_AUTOHINT; 200 | 201 | if (opts.use_color_info) load_flags |= m.FT_LOAD_COLOR; 202 | 203 | let error = FT_Load_Glyph(face.ptr, glyph_index, load_flags); 204 | 205 | if (error) throw new Error(`error in FT_Load_Glyph: ${error}`); 206 | 207 | let glyph = m.getValue(face.ptr + m.OFFSET_FACE_GLYPH, 'i32'); 208 | 209 | let glyph_data = { 210 | glyph_index: m.getValue(glyph + m.OFFSET_GLYPH_INDEX, 'i32'), 211 | metrics: { 212 | width: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_WIDTH, 'i32')), 213 | height: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_HEIGHT, 'i32')), 214 | horiBearingX: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_HORI_BEARING_X, 'i32')), 215 | horiBearingY: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_HORI_BEARING_Y, 'i32')), 216 | horiAdvance: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_HORI_ADVANCE, 'i32')), 217 | vertBearingX: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_VERT_BEARING_X, 'i32')), 218 | vertBearingY: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_VERT_BEARING_Y, 'i32')), 219 | vertAdvance: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_METRICS_VERT_ADVANCE, 'i32')) 220 | }, 221 | linearHoriAdvance: from_16_16(m.getValue(glyph + m.OFFSET_GLYPH_LINEAR_HORI_ADVANCE, 'i32')), 222 | linearVertAdvance: from_16_16(m.getValue(glyph + m.OFFSET_GLYPH_LINEAR_VERT_ADVANCE, 'i32')), 223 | advance: { 224 | x: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_ADVANCE_X, 'i32')), 225 | y: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_ADVANCE_Y, 'i32')) 226 | }, 227 | bitmap: { 228 | width: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_WIDTH, 'i32'), 229 | rows: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_ROWS, 'i32'), 230 | pitch: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_PITCH, 'i32'), 231 | num_grays: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_NUM_GRAYS, 'i16'), 232 | pixel_mode: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_PIXEL_MODE, 'i8'), 233 | palette_mode: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_PALETTE_MODE, 'i8') 234 | }, 235 | bitmap_left: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_LEFT, 'i32'), 236 | bitmap_top: m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_TOP, 'i32'), 237 | lsb_delta: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_LSB_DELTA, 'i32')), 238 | rsb_delta: from_26_6(m.getValue(glyph + m.OFFSET_GLYPH_RSB_DELTA, 'i32')) 239 | }; 240 | 241 | let g_w = glyph_data.bitmap.width; 242 | let g_h = glyph_data.bitmap.rows; 243 | let g_x = glyph_data.bitmap_left; 244 | let g_y = glyph_data.bitmap_top; 245 | 246 | let buffer = m.getValue(glyph + m.OFFSET_GLYPH_BITMAP_BUFFER, 'i32'); 247 | let pitch = Math.abs(glyph_data.bitmap.pitch); 248 | 249 | let advance_x = glyph_data.linearHoriAdvance; 250 | let advance_y = glyph_data.linearVertAdvance; 251 | 252 | let pixel_mode = glyph_data.bitmap.pixel_mode; 253 | 254 | let output = []; 255 | 256 | for (let y = 0; y < g_h; y++) { 257 | let row_start = buffer + y * pitch; 258 | let line = []; 259 | 260 | for (let x = 0; x < g_w; x++) { 261 | if (pixel_mode === m.FT_PIXEL_MODE_MONO) { 262 | let value = m.getValue(row_start + ~~(x / 8), 'i8'); 263 | line.push(value & (1 << (7 - (x % 8))) ? 255 : 0); 264 | } else if (pixel_mode === m.FT_PIXEL_MODE_BGRA) { 265 | let blue = int8_to_uint8(m.getValue(row_start + (x * 4) + 0, 'i8')); 266 | let green = int8_to_uint8(m.getValue(row_start + (x * 4) + 1, 'i8')); 267 | let red = int8_to_uint8(m.getValue(row_start + (x * 4) + 2, 'i8')); 268 | let alpha = int8_to_uint8(m.getValue(row_start + (x * 4) + 3, 'i8')); 269 | // convert RGBA to grayscale 270 | let grayscale = Math.round(0.299 * red + 0.587 * green + 0.114 * blue); 271 | if (grayscale > 255) grayscale = 255; 272 | // meld grayscale into alpha channel 273 | alpha = ((255 - grayscale) * alpha) / 255; 274 | line.push(alpha); 275 | } else { 276 | let value = m.getValue(row_start + x, 'i8'); 277 | line.push(int8_to_uint8(value)); 278 | } 279 | } 280 | 281 | output.push(line); 282 | } 283 | 284 | return { 285 | x: g_x, 286 | y: g_y, 287 | width: g_w, 288 | height: g_h, 289 | advance_x, 290 | advance_y, 291 | pixels: output, 292 | freetype: glyph_data 293 | }; 294 | }; 295 | 296 | 297 | module.exports.fontface_destroy = function (face) { 298 | let error = FT_Done_Face(face.ptr); 299 | 300 | if (error) throw new Error(`error in FT_Done_Face: ${error}`); 301 | 302 | m._free(face.font); 303 | face.ptr = 0; 304 | face.font = 0; 305 | }; 306 | 307 | 308 | module.exports.destroy = function () { 309 | let error = m.ccall('FT_Done_FreeType', 'number', [ 'number' ], [ library ]); 310 | 311 | if (error) throw new Error(`error in FT_Done_FreeType: ${error}`); 312 | 313 | library = 0; 314 | 315 | // don't unload wasm - slows down tests too much 316 | //m = null; 317 | }; 318 | -------------------------------------------------------------------------------- /lib/freetype/render.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include FT_FREETYPE_H 4 | #include FT_TRUETYPE_TABLES_H 5 | 6 | static void set_js_variable(char* name, int value) { 7 | char buffer[strlen(name) + 32]; 8 | sprintf(buffer, "Module.%s = %d;", name, value); 9 | emscripten_run_script(buffer); 10 | } 11 | 12 | // Expose constants, used in calls from js 13 | void init_constants() 14 | { 15 | set_js_variable("FT_LOAD_DEFAULT", FT_LOAD_DEFAULT); 16 | set_js_variable("FT_LOAD_NO_HINTING", FT_LOAD_NO_HINTING); 17 | set_js_variable("FT_LOAD_RENDER", FT_LOAD_RENDER); 18 | set_js_variable("FT_LOAD_FORCE_AUTOHINT", FT_LOAD_FORCE_AUTOHINT); 19 | set_js_variable("FT_LOAD_PEDANTIC", FT_LOAD_PEDANTIC); 20 | set_js_variable("FT_LOAD_MONOCHROME", FT_LOAD_MONOCHROME); 21 | set_js_variable("FT_LOAD_NO_AUTOHINT", FT_LOAD_NO_AUTOHINT); 22 | set_js_variable("FT_LOAD_COLOR", FT_LOAD_COLOR); 23 | 24 | set_js_variable("FT_LOAD_TARGET_NORMAL", FT_LOAD_TARGET_NORMAL); 25 | set_js_variable("FT_LOAD_TARGET_LIGHT", FT_LOAD_TARGET_LIGHT); 26 | set_js_variable("FT_LOAD_TARGET_MONO", FT_LOAD_TARGET_MONO); 27 | set_js_variable("FT_LOAD_TARGET_LCD", FT_LOAD_TARGET_LCD); 28 | set_js_variable("FT_LOAD_TARGET_LCD_V", FT_LOAD_TARGET_LCD_V); 29 | 30 | set_js_variable("FT_RENDER_MODE_NORMAL", FT_RENDER_MODE_NORMAL); 31 | set_js_variable("FT_RENDER_MODE_MONO", FT_RENDER_MODE_MONO); 32 | set_js_variable("FT_RENDER_MODE_LCD", FT_RENDER_MODE_LCD); 33 | set_js_variable("FT_RENDER_MODE_LCD_V", FT_RENDER_MODE_LCD_V); 34 | 35 | set_js_variable("FT_KERNING_DEFAULT", FT_KERNING_DEFAULT); 36 | set_js_variable("FT_KERNING_UNFITTED", FT_KERNING_UNFITTED); 37 | set_js_variable("FT_KERNING_UNSCALED", FT_KERNING_UNSCALED); 38 | 39 | set_js_variable("FT_SFNT_OS2", FT_SFNT_OS2); 40 | 41 | set_js_variable("FT_FACE_FLAG_COLOR", FT_FACE_FLAG_COLOR); 42 | 43 | set_js_variable("FT_PIXEL_MODE_MONO", FT_PIXEL_MODE_MONO); 44 | set_js_variable("FT_PIXEL_MODE_BGRA", FT_PIXEL_MODE_BGRA); 45 | 46 | set_js_variable("OFFSET_FACE_GLYPH", offsetof(FT_FaceRec, glyph)); 47 | set_js_variable("OFFSET_FACE_UNITS_PER_EM", offsetof(FT_FaceRec, units_per_EM)); 48 | set_js_variable("OFFSET_FACE_ASCENDER", offsetof(FT_FaceRec, ascender)); 49 | set_js_variable("OFFSET_FACE_DESCENDER", offsetof(FT_FaceRec, descender)); 50 | set_js_variable("OFFSET_FACE_HEIGHT", offsetof(FT_FaceRec, height)); 51 | set_js_variable("OFFSET_FACE_FACE_FLAGS", offsetof(FT_FaceRec, face_flags)); 52 | 53 | set_js_variable("OFFSET_GLYPH_BITMAP_WIDTH", offsetof(FT_GlyphSlotRec, bitmap.width)); 54 | set_js_variable("OFFSET_GLYPH_BITMAP_ROWS", offsetof(FT_GlyphSlotRec, bitmap.rows)); 55 | set_js_variable("OFFSET_GLYPH_BITMAP_PITCH", offsetof(FT_GlyphSlotRec, bitmap.pitch)); 56 | set_js_variable("OFFSET_GLYPH_BITMAP_BUFFER", offsetof(FT_GlyphSlotRec, bitmap.buffer)); 57 | set_js_variable("OFFSET_GLYPH_BITMAP_NUM_GRAYS", offsetof(FT_GlyphSlotRec, bitmap.num_grays)); 58 | set_js_variable("OFFSET_GLYPH_BITMAP_PIXEL_MODE", offsetof(FT_GlyphSlotRec, bitmap.pixel_mode)); 59 | set_js_variable("OFFSET_GLYPH_BITMAP_PALETTE_MODE", offsetof(FT_GlyphSlotRec, bitmap.palette_mode)); 60 | 61 | set_js_variable("OFFSET_GLYPH_METRICS_WIDTH", offsetof(FT_GlyphSlotRec, metrics.width)); 62 | set_js_variable("OFFSET_GLYPH_METRICS_HEIGHT", offsetof(FT_GlyphSlotRec, metrics.height)); 63 | set_js_variable("OFFSET_GLYPH_METRICS_HORI_BEARING_X", offsetof(FT_GlyphSlotRec, metrics.horiBearingX)); 64 | set_js_variable("OFFSET_GLYPH_METRICS_HORI_BEARING_Y", offsetof(FT_GlyphSlotRec, metrics.horiBearingY)); 65 | set_js_variable("OFFSET_GLYPH_METRICS_HORI_ADVANCE", offsetof(FT_GlyphSlotRec, metrics.horiAdvance)); 66 | set_js_variable("OFFSET_GLYPH_METRICS_VERT_BEARING_X", offsetof(FT_GlyphSlotRec, metrics.vertBearingX)); 67 | set_js_variable("OFFSET_GLYPH_METRICS_VERT_BEARING_Y", offsetof(FT_GlyphSlotRec, metrics.vertBearingY)); 68 | set_js_variable("OFFSET_GLYPH_METRICS_VERT_ADVANCE", offsetof(FT_GlyphSlotRec, metrics.vertAdvance)); 69 | 70 | set_js_variable("OFFSET_GLYPH_BITMAP_LEFT", offsetof(FT_GlyphSlotRec, bitmap_left)); 71 | set_js_variable("OFFSET_GLYPH_BITMAP_TOP", offsetof(FT_GlyphSlotRec, bitmap_top)); 72 | set_js_variable("OFFSET_GLYPH_INDEX", offsetof(FT_GlyphSlotRec, glyph_index)); 73 | set_js_variable("OFFSET_GLYPH_LINEAR_HORI_ADVANCE", offsetof(FT_GlyphSlotRec, linearHoriAdvance)); 74 | set_js_variable("OFFSET_GLYPH_LINEAR_VERT_ADVANCE", offsetof(FT_GlyphSlotRec, linearVertAdvance)); 75 | set_js_variable("OFFSET_GLYPH_ADVANCE_X", offsetof(FT_GlyphSlotRec, advance.x)); 76 | set_js_variable("OFFSET_GLYPH_ADVANCE_Y", offsetof(FT_GlyphSlotRec, advance.y)); 77 | set_js_variable("OFFSET_GLYPH_LSB_DELTA", offsetof(FT_GlyphSlotRec, lsb_delta)); 78 | set_js_variable("OFFSET_GLYPH_RSB_DELTA", offsetof(FT_GlyphSlotRec, rsb_delta)); 79 | 80 | set_js_variable("OFFSET_TT_OS2_ASCENDER", offsetof(TT_OS2, sTypoAscender)); 81 | set_js_variable("OFFSET_TT_OS2_DESCENDER", offsetof(TT_OS2, sTypoDescender)); 82 | set_js_variable("OFFSET_TT_OS2_LINEGAP", offsetof(TT_OS2, sTypoLineGap)); 83 | } 84 | -------------------------------------------------------------------------------- /lib/ranger.js: -------------------------------------------------------------------------------- 1 | // Merge ranges into single object 2 | 3 | 'use strict'; 4 | 5 | 6 | class Ranger { 7 | constructor() { 8 | this.data = {}; 9 | } 10 | 11 | // input: 12 | // -r 0x1F450 - single value, dec or hex format 13 | // -r 0x1F450-0x1F470 - range 14 | // -r 0x1F450=>0xF005 - single glyph with mapping 15 | // -r 0x1F450-0x1F470=>0xF005 - range with mapping 16 | add_range(font, start, end, mapped_start) { 17 | let offset = mapped_start - start; 18 | let output = []; 19 | 20 | for (let i = start; i <= end; i++) { 21 | this._set_char(font, i, i + offset); 22 | output.push(i); 23 | } 24 | 25 | return output; 26 | } 27 | 28 | // input: characters to copy, e.g. '1234567890abcdef' 29 | add_symbols(font, str) { 30 | let output = []; 31 | 32 | for (let chr of str) { 33 | let code = chr.codePointAt(0); 34 | this._set_char(font, code, code); 35 | output.push(code); 36 | } 37 | 38 | return output; 39 | } 40 | 41 | _set_char(font, code, mapped_to) { 42 | this.data[mapped_to] = { font, code }; 43 | } 44 | 45 | get() { 46 | return this.data; 47 | } 48 | } 49 | 50 | 51 | module.exports = Ranger; 52 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | function set_byte_depth(depth) { 5 | return function (byte) { 6 | // calculate significant bits, e.g. for depth=2 it's 0, 1, 2 or 3 7 | let value = ~~(byte / (256 >> depth)); 8 | 9 | // spread those bits around 0..255 range, e.g. for depth=2 it's 0, 85, 170 or 255 10 | let scale = (2 << (depth - 1)) - 1; 11 | 12 | return (value * 0xFFFF / scale) >> 8; 13 | }; 14 | } 15 | 16 | 17 | module.exports.set_depth = function set_depth(glyph, depth) { 18 | let pixels = []; 19 | let fn = set_byte_depth(depth); 20 | 21 | for (let y = 0; y < glyph.bbox.height; y++) { 22 | pixels.push(glyph.pixels[y].map(fn)); 23 | } 24 | 25 | return Object.assign({}, glyph, { pixels }); 26 | }; 27 | 28 | 29 | function count_bits(val) { 30 | let count = 0; 31 | val = ~~val; 32 | 33 | while (val) { 34 | count++; 35 | val >>= 1; 36 | } 37 | 38 | return count; 39 | } 40 | 41 | // Minimal number of bits to store unsigned value 42 | module.exports.unsigned_bits = count_bits; 43 | 44 | // Minimal number of bits to store signed value 45 | module.exports.signed_bits = function signed_bits(val) { 46 | if (val >= 0) return count_bits(val) + 1; 47 | 48 | return count_bits(Math.abs(val) - 1) + 1; 49 | }; 50 | 51 | // Align value to 4x - useful to create word-aligned arrays 52 | function align4(size) { 53 | if (size % 4 === 0) return size; 54 | return size + 4 - (size % 4); 55 | } 56 | module.exports.align4 = align4; 57 | 58 | // Align buffer length to 4x (returns copy with zero-filled tail) 59 | module.exports.balign4 = function balign4(buf) { 60 | let buf_aligned = Buffer.alloc(align4(buf.length)); 61 | buf.copy(buf_aligned); 62 | return buf_aligned; 63 | }; 64 | 65 | // Pre-filter image to improve compression ratio 66 | // In this case - XOR lines, because it's very effective 67 | // in decompressor and does not depend on bpp. 68 | module.exports.prefilter = function prefilter(pixels) { 69 | return pixels.map((line, l_idx, arr) => { 70 | if (l_idx === 0) return line.slice(); 71 | 72 | return line.map((p, idx) => p ^ arr[l_idx - 1][idx]); 73 | }); 74 | }; 75 | 76 | 77 | // Convert array with uint16 data to buffer 78 | module.exports.bFromA16 = function bFromA16(arr) { 79 | const buf = Buffer.alloc(arr.length * 2); 80 | 81 | for (let i = 0; i < arr.length; i++) buf.writeUInt16LE(arr[i], i * 2); 82 | 83 | return buf; 84 | }; 85 | 86 | // Convert array with uint32 data to buffer 87 | module.exports.bFromA32 = function bFromA32(arr) { 88 | const buf = Buffer.alloc(arr.length * 4); 89 | 90 | for (let i = 0; i < arr.length; i++) buf.writeUInt32LE(arr[i], i * 4); 91 | 92 | return buf; 93 | }; 94 | 95 | 96 | function chunk(arr, size) { 97 | const result = []; 98 | for (let i = 0; i < arr.length; i += size) { 99 | result.push(arr.slice(i, i + size)); 100 | } 101 | return result; 102 | } 103 | 104 | // Dump long array to multiline format with X columns and Y indent 105 | module.exports.long_dump = function long_dump(arr, options = {}) { 106 | const defaults = { 107 | col: 8, 108 | indent: 4, 109 | hex: false 110 | }; 111 | 112 | let opts = Object.assign({}, defaults, options); 113 | let indent = ' '.repeat(opts.indent); 114 | 115 | return chunk(Array.from(arr), opts.col) 116 | .map(l => l.map(v => (opts.hex ? `0x${v.toString(16)}` : v.toString()))) 117 | .map(l => `${indent}${l.join(', ')}`) 118 | .join(',\n'); 119 | }; 120 | 121 | // stable sort by pick() result 122 | module.exports.sort_by = function sort_by(arr, pick) { 123 | return arr 124 | .map((el, idx) => ({ el, idx })) 125 | .sort((a, b) => (pick(a.el) - pick(b.el)) || (a.idx - b.idx)) 126 | .map(({ el }) => el); 127 | }; 128 | 129 | module.exports.sum = function sum(arr) { 130 | return arr.reduce((a, v) => a + v, 0); 131 | }; 132 | -------------------------------------------------------------------------------- /lib/writers/bin.js: -------------------------------------------------------------------------------- 1 | // Write font in binary format 2 | 'use strict'; 3 | 4 | 5 | const AppError = require('../app_error'); 6 | const Font = require('../font/font'); 7 | 8 | 9 | module.exports = function write_images(args, fontData) { 10 | if (!args.output) throw new AppError('Output is required for "bin" writer'); 11 | 12 | const font = new Font(fontData, args); 13 | 14 | return { 15 | [args.output]: font.toBin() 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/writers/dump.js: -------------------------------------------------------------------------------- 1 | // Write font data into png images 2 | 3 | 'use strict'; 4 | 5 | 6 | const path = require('path'); 7 | const { PNG } = require('pngjs'); 8 | const AppError = require('../app_error'); 9 | const utils = require('../utils'); 10 | 11 | const normal_color = [ 255, 255, 255 ]; 12 | const outside_color = [ 255, 127, 184 ]; 13 | 14 | 15 | module.exports = function write_images(args, font) { 16 | if (!args.output) throw new AppError('Output is required for "dump" writer'); 17 | 18 | let files = {}; 19 | 20 | let glyphs = font.glyphs.map(glyph => utils.set_depth(glyph, args.bpp)); 21 | 22 | for (let glyph of glyphs) { 23 | let { code, advanceWidth, bbox, pixels } = glyph; 24 | 25 | advanceWidth = Math.round(advanceWidth); 26 | 27 | let minX = bbox.x; 28 | let maxX = Math.max(bbox.x + bbox.width - 1, bbox.x); 29 | let minY = Math.min(bbox.y, font.typoDescent); 30 | let maxY = Math.max(bbox.y + bbox.height - 1, font.typoAscent); 31 | 32 | let png = new PNG({ width: maxX - minX + 1, height: maxY - minY + 1 }); 33 | 34 | /* eslint-disable max-depth */ 35 | for (let pos = 0, y = maxY; y >= minY; y--) { 36 | for (let x = minX; x <= maxX; x++) { 37 | let value = 0; 38 | 39 | if (x >= bbox.x && x < bbox.x + bbox.width && y >= bbox.y && y < bbox.y + bbox.height) { 40 | value = pixels[bbox.height - (y - bbox.y) - 1][x - bbox.x]; 41 | } 42 | 43 | let r, g, b; 44 | 45 | if (x < 0 || x >= advanceWidth || y < font.typoDescent || y > font.typoAscent) { 46 | [ r, g, b ] = outside_color; 47 | } else { 48 | [ r, g, b ] = normal_color; 49 | } 50 | 51 | png.data[pos++] = (255 - value) * r / 255; 52 | png.data[pos++] = (255 - value) * g / 255; 53 | png.data[pos++] = (255 - value) * b / 255; 54 | png.data[pos++] = 255; 55 | } 56 | } 57 | 58 | 59 | files[path.join(args.output, `${code.toString(16)}.png`)] = PNG.sync.write(png); 60 | } 61 | 62 | files[path.join(args.output, 'font_info.json')] = JSON.stringify( 63 | font, 64 | (k, v) => (k === 'pixels' && !args.full_info ? undefined : v), 65 | 2); 66 | 67 | return files; 68 | }; 69 | -------------------------------------------------------------------------------- /lib/writers/lvgl/index.js: -------------------------------------------------------------------------------- 1 | // Write font in lvgl format 2 | 'use strict'; 3 | 4 | 5 | const AppError = require('../../app_error'); 6 | const Font = require('./lv_font'); 7 | 8 | 9 | module.exports = function write_images(args, fontData) { 10 | if (!args.output) throw new AppError('Output is required for "lvgl" writer'); 11 | 12 | const font = new Font(fontData, args); 13 | 14 | return { 15 | [args.output]: font.toLVGL() 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/writers/lvgl/lv_font.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const path = require('path'); 5 | 6 | const Font = require('../../font/font'); 7 | const Head = require('./lv_table_head'); 8 | const Cmap = require('./lv_table_cmap'); 9 | const Glyf = require('./lv_table_glyf'); 10 | const Kern = require('./lv_table_kern'); 11 | const AppError = require('../../app_error'); 12 | 13 | 14 | class LvFont extends Font { 15 | constructor(fontData, options) { 16 | super(fontData, options); 17 | 18 | this.font_name = options.lv_font_name; 19 | if (!this.font_name) { 20 | const ext = path.extname(options.output); 21 | this.font_name = path.basename(options.output, ext); 22 | } 23 | 24 | if (options.lv_fallback) { 25 | this.fallback = '&' + options.lv_fallback; 26 | this.fallback_declaration = 'extern const lv_font_t ' + options.lv_fallback + ';\n'; 27 | } else { 28 | this.fallback = 'NULL'; 29 | this.fallback_declaration = ''; 30 | } 31 | 32 | if (options.bpp === 3 & options.no_compress) { 33 | throw new AppError('LVGL supports "--bpp 3" with compression only'); 34 | } 35 | } 36 | 37 | init_tables() { 38 | this.head = new Head(this); 39 | this.glyf = new Glyf(this); 40 | this.cmap = new Cmap(this); 41 | this.kern = new Kern(this); 42 | } 43 | 44 | stride_guard() { 45 | if (this.opts.stride !== 1) { 46 | return `#if !LV_VERSION_CHECK(9, 3, 0) 47 | #error "At least LVGL v9.3 is required to use the stride attribute of the fonts" 48 | #endif`; 49 | } 50 | 51 | return ''; 52 | } 53 | 54 | large_format_guard() { 55 | let guard_required = false; 56 | let glyphs_bin_size = 0; 57 | 58 | this.glyf.lv_data.forEach(d => { 59 | glyphs_bin_size += d.bin.length; 60 | 61 | if (d.glyph.bbox.width > 255 || 62 | d.glyph.bbox.height > 255 || 63 | Math.abs(d.glyph.bbox.x) > 127 || 64 | Math.abs(d.glyph.bbox.y) > 127 || 65 | Math.round(d.glyph.advanceWidth * 16) > 4096) { 66 | guard_required = true; 67 | } 68 | }); 69 | 70 | if (glyphs_bin_size > 1024 * 1024) guard_required = true; 71 | 72 | if (!guard_required) return ''; 73 | 74 | return ` 75 | #if (LV_FONT_FMT_TXT_LARGE == 0) 76 | # error "Too large font or glyphs in ${this.font_name.toUpperCase()}. Enable LV_FONT_FMT_TXT_LARGE in lv_conf.h") 77 | #endif 78 | `.trimLeft(); 79 | } 80 | 81 | toLVGL() { 82 | let guard_name = this.font_name.toUpperCase(); 83 | 84 | return `/******************************************************************************* 85 | * Size: ${this.src.size} px 86 | * Bpp: ${this.opts.bpp} 87 | * Opts: ${this.opts.opts_string} 88 | ******************************************************************************/ 89 | 90 | #ifdef __has_include 91 | #if __has_include("lvgl.h") 92 | #ifndef LV_LVGL_H_INCLUDE_SIMPLE 93 | #define LV_LVGL_H_INCLUDE_SIMPLE 94 | #endif 95 | #endif 96 | #endif 97 | 98 | #ifdef LV_LVGL_H_INCLUDE_SIMPLE 99 | #include "lvgl.h" 100 | #else 101 | #include "${this.opts.lv_include || 'lvgl/lvgl.h'}" 102 | #endif 103 | 104 | ${this.stride_guard()} 105 | 106 | #ifndef ${guard_name} 107 | #define ${guard_name} 1 108 | #endif 109 | 110 | #if ${guard_name} 111 | 112 | ${this.glyf.toLVGL()} 113 | 114 | ${this.cmap.toLVGL()} 115 | 116 | ${this.kern.toLVGL()} 117 | 118 | ${this.head.toLVGL()} 119 | 120 | ${this.large_format_guard()} 121 | 122 | #endif /*#if ${guard_name}*/ 123 | `; 124 | } 125 | } 126 | 127 | 128 | module.exports = LvFont; 129 | -------------------------------------------------------------------------------- /lib/writers/lvgl/lv_table_cmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const u = require('../../utils'); 5 | const build_subtables = require('../../font/cmap_build_subtables'); 6 | const Cmap = require('../../font/table_cmap'); 7 | 8 | 9 | class LvCmap extends Cmap { 10 | constructor(font) { 11 | super(font); 12 | 13 | this.lv_compiled = false; 14 | this.lv_subtables = []; 15 | } 16 | 17 | lv_format2enum(name) { 18 | switch (name) { 19 | case 'format0_tiny': return 'LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY'; 20 | case 'format0': return 'LV_FONT_FMT_TXT_CMAP_FORMAT0_FULL'; 21 | case 'sparse_tiny': return 'LV_FONT_FMT_TXT_CMAP_SPARSE_TINY'; 22 | case 'sparse': return 'LV_FONT_FMT_TXT_CMAP_SPARSE_FULL'; 23 | default: throw new Error('Unknown subtable format'); 24 | } 25 | } 26 | 27 | lv_compile() { 28 | if (this.lv_compiled) return; 29 | this.lv_compiled = true; 30 | 31 | const f = this.font; 32 | 33 | let subtables_plan = build_subtables(f.src.glyphs.map(g => g.code)); 34 | let idx = 0; 35 | 36 | for (let [ format, codepoints ] of subtables_plan) { 37 | let g = this.glyphByCode(codepoints[0]); 38 | let start_glyph_id = f.glyph_id[g.code]; 39 | let min_code = codepoints[0]; 40 | let max_code = codepoints[codepoints.length - 1]; 41 | 42 | let has_charcodes = false; 43 | let has_ids = false; 44 | let defs = ''; 45 | let entries_count = 0; 46 | 47 | if (format === 'format0_tiny') { 48 | // use default empty values 49 | } else if (format === 'format0') { 50 | has_ids = true; 51 | let d = this.collect_format0_data(min_code, max_code, start_glyph_id); 52 | entries_count = d.length; 53 | 54 | defs = ` 55 | static const uint8_t glyph_id_ofs_list_${idx}[] = { 56 | ${u.long_dump(d)} 57 | }; 58 | `.trim(); 59 | 60 | } else if (format === 'sparse_tiny') { 61 | has_charcodes = true; 62 | let d = this.collect_sparse_data(codepoints, start_glyph_id); 63 | entries_count = d.codes.length; 64 | 65 | defs = ` 66 | static const uint16_t unicode_list_${idx}[] = { 67 | ${u.long_dump(d.codes, { hex: true })} 68 | }; 69 | `.trim(); 70 | 71 | } else { // assume format === 'sparse' 72 | has_charcodes = true; 73 | has_ids = true; 74 | let d = this.collect_sparse_data(codepoints, start_glyph_id); 75 | entries_count = d.codes.length; 76 | 77 | defs = ` 78 | static const uint16_t unicode_list_${idx}[] = { 79 | ${u.long_dump(d.codes, { hex: true })} 80 | }; 81 | static const uint16_t glyph_id_ofs_list_${idx}[] = { 82 | ${u.long_dump(d.ids)} 83 | }; 84 | `.trim(); 85 | } 86 | 87 | const u_list = has_charcodes ? `unicode_list_${idx}` : 'NULL'; 88 | const id_list = has_ids ? `glyph_id_ofs_list_${idx}` : 'NULL'; 89 | 90 | /* eslint-disable max-len */ 91 | const head = ` { 92 | .range_start = ${min_code}, .range_length = ${max_code - min_code + 1}, .glyph_id_start = ${start_glyph_id}, 93 | .unicode_list = ${u_list}, .glyph_id_ofs_list = ${id_list}, .list_length = ${entries_count}, .type = ${this.lv_format2enum(format)} 94 | }`; 95 | 96 | this.lv_subtables.push({ 97 | defs, 98 | head 99 | }); 100 | 101 | idx++; 102 | } 103 | } 104 | 105 | toLVGL() { 106 | this.lv_compile(); 107 | 108 | return ` 109 | /*--------------------- 110 | * CHARACTER MAPPING 111 | *--------------------*/ 112 | 113 | ${this.lv_subtables.map(d => d.defs).filter(Boolean).join('\n\n')} 114 | 115 | /*Collect the unicode lists and glyph_id offsets*/ 116 | static const lv_font_fmt_txt_cmap_t cmaps[] = 117 | { 118 | ${this.lv_subtables.map(d => d.head).join(',\n')} 119 | }; 120 | `.trim(); 121 | } 122 | } 123 | 124 | 125 | module.exports = LvCmap; 126 | -------------------------------------------------------------------------------- /lib/writers/lvgl/lv_table_glyf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const { BitStream } = require('bit-buffer'); 5 | const u = require('../../utils'); 6 | const Glyf = require('../../font/table_glyf'); 7 | 8 | 9 | class LvGlyf extends Glyf { 10 | constructor(font) { 11 | super(font); 12 | 13 | this.lv_data = []; 14 | this.lv_compiled = false; 15 | } 16 | 17 | lv_bitmap(glyph) { 18 | let buf; 19 | 20 | if (this.font.opts.align !== 1 || this.font.opts.stride !== 1) { 21 | buf = Buffer.alloc(100 + glyph.bbox.width * glyph.bbox.height * 4 * 4); 22 | } else { 23 | buf = Buffer.alloc(100 + glyph.bbox.width * glyph.bbox.height * 4); 24 | } 25 | 26 | const bs = new BitStream(buf); 27 | bs.bigEndian = true; 28 | 29 | const pixels = this.font.glyf.pixelsToBpp(glyph.pixels); 30 | 31 | this.font.glyf.storePixels(bs, pixels); 32 | 33 | let glyph_bitmap; 34 | if (this.font.opts.align !== 1) { 35 | glyph_bitmap = Buffer.alloc(Math.ceil(bs.byteIndex / this.font.opts.align) * this.font.opts.align); 36 | } else { 37 | glyph_bitmap = Buffer.alloc(bs.byteIndex); 38 | } 39 | 40 | buf.copy(glyph_bitmap, 0, 0, bs.byteIndex); 41 | 42 | return glyph_bitmap; 43 | } 44 | 45 | lv_compile() { 46 | if (this.lv_compiled) return; 47 | 48 | this.lv_compiled = true; 49 | 50 | const f = this.font; 51 | this.lv_data = []; 52 | let offset = 0; 53 | 54 | f.src.glyphs.forEach(g => { 55 | const id = f.glyph_id[g.code]; 56 | const bin = this.lv_bitmap(g); 57 | this.lv_data[id] = { 58 | bin, 59 | offset, 60 | glyph: g 61 | }; 62 | offset += bin.length; 63 | }); 64 | } 65 | 66 | to_lv_bitmaps() { 67 | this.lv_compile(); 68 | 69 | let result = []; 70 | this.lv_data.forEach((d, idx) => { 71 | if (idx === 0) return; 72 | const code_hex = d.glyph.code.toString(16).toUpperCase(); 73 | const code_str = JSON.stringify(String.fromCodePoint(d.glyph.code)); 74 | 75 | let txt = ` /* U+${code_hex.padStart(4, '0')} ${code_str} */ 76 | ${u.long_dump(d.bin, { hex: true })}`; 77 | 78 | if (idx < this.lv_data.length - 1) { 79 | // skip comma for zero data 80 | txt += d.bin.length ? ',\n\n' : '\n'; 81 | } 82 | 83 | result.push(txt); 84 | }); 85 | 86 | return result.join(''); 87 | } 88 | 89 | to_lv_glyph_dsc() { 90 | this.lv_compile(); 91 | 92 | /* eslint-disable max-len */ 93 | 94 | let result = [ ' {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */' ]; 95 | 96 | this.lv_data.forEach(d => { 97 | const idx = d.offset, 98 | adv_w = Math.round(d.glyph.advanceWidth * 16), 99 | h = d.glyph.bbox.height, 100 | w = d.glyph.bbox.width, 101 | x = d.glyph.bbox.x, 102 | y = d.glyph.bbox.y; 103 | result.push(` {.bitmap_index = ${idx}, .adv_w = ${adv_w}, .box_w = ${w}, .box_h = ${h}, .ofs_x = ${x}, .ofs_y = ${y}}`); 104 | }); 105 | 106 | return result.join(',\n'); 107 | } 108 | 109 | 110 | toLVGL() { 111 | return ` 112 | /*----------------- 113 | * BITMAPS 114 | *----------------*/ 115 | 116 | /*Store the image of the glyphs*/ 117 | static ${this.font.opts.align !== 1 ? 'LV_ATTRIBUTE_MEM_ALIGN ' : ''}LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 118 | ${this.to_lv_bitmaps()} 119 | }; 120 | 121 | 122 | /*--------------------- 123 | * GLYPH DESCRIPTION 124 | *--------------------*/ 125 | 126 | static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { 127 | ${this.to_lv_glyph_dsc()} 128 | }; 129 | `.trim(); 130 | } 131 | } 132 | 133 | 134 | module.exports = LvGlyf; 135 | -------------------------------------------------------------------------------- /lib/writers/lvgl/lv_table_head.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const Head = require('../../font/table_head'); 5 | 6 | 7 | class LvHead extends Head { 8 | constructor(font) { 9 | super(font); 10 | } 11 | 12 | kern_ref() { 13 | const f = this.font; 14 | 15 | if (!f.hasKerning()) { 16 | return { 17 | scale: '0', 18 | dsc: 'NULL', 19 | classes: '0' 20 | }; 21 | } 22 | 23 | if (!f.kern.should_use_format3()) { 24 | return { 25 | scale: `${Math.round(f.kerningScale * 16)}`, 26 | dsc: '&kern_pairs', 27 | classes: '0' 28 | }; 29 | } 30 | 31 | return { 32 | scale: `${Math.round(f.kerningScale * 16)}`, 33 | dsc: '&kern_classes', 34 | classes: '1' 35 | }; 36 | } 37 | 38 | get_stride_align() { 39 | if (this.font.opts.stride !== 1) { 40 | return ` .stride = ${this.font.opts.stride}`; 41 | } 42 | return ''; 43 | } 44 | 45 | get_static_bitmap() { 46 | if (this.font.opts.stride !== 1 && this.font.opts.align !== 1) { 47 | return ' .static_bitmap = 1,'; 48 | } 49 | return ' .static_bitmap = 0,'; 50 | } 51 | 52 | toLVGL() { 53 | const f = this.font; 54 | const kern = this.kern_ref(); 55 | const subpixels = (f.subpixels_mode === 0) ? 'LV_FONT_SUBPX_NONE' : 56 | (f.subpixels_mode === 1) ? 'LV_FONT_SUBPX_HOR' : 'LV_FONT_SUBPX_VER'; 57 | 58 | return ` 59 | /*-------------------- 60 | * ALL CUSTOM DATA 61 | *--------------------*/ 62 | 63 | #if LVGL_VERSION_MAJOR == 8 64 | /*Store all the custom data of the font*/ 65 | static lv_font_fmt_txt_glyph_cache_t cache; 66 | #endif 67 | 68 | #if LVGL_VERSION_MAJOR >= 8 69 | static const lv_font_fmt_txt_dsc_t font_dsc = { 70 | #else 71 | static lv_font_fmt_txt_dsc_t font_dsc = { 72 | #endif 73 | .glyph_bitmap = glyph_bitmap, 74 | .glyph_dsc = glyph_dsc, 75 | .cmaps = cmaps, 76 | .kern_dsc = ${kern.dsc}, 77 | .kern_scale = ${kern.scale}, 78 | .cmap_num = ${f.cmap.toBin().readUInt32LE(8)}, 79 | .bpp = ${f.opts.bpp}, 80 | .kern_classes = ${kern.classes}, 81 | .bitmap_format = ${f.glyf.getCompressionCode()}, 82 | #if LVGL_VERSION_MAJOR == 8 83 | .cache = &cache 84 | #endif 85 | ${this.get_stride_align()} 86 | }; 87 | 88 | ${f.fallback_declaration} 89 | 90 | /*----------------- 91 | * PUBLIC FONT 92 | *----------------*/ 93 | 94 | /*Initialize a public general font descriptor*/ 95 | #if LVGL_VERSION_MAJOR >= 8 96 | const lv_font_t ${f.font_name} = { 97 | #else 98 | lv_font_t ${f.font_name} = { 99 | #endif 100 | .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ 101 | .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ 102 | .line_height = ${f.src.ascent - f.src.descent}, /*The maximum line height required by the font*/ 103 | .base_line = ${-f.src.descent}, /*Baseline measured from the bottom of the line*/ 104 | #if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) 105 | .subpx = ${subpixels}, 106 | #endif 107 | #if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 108 | .underline_position = ${f.src.underlinePosition}, 109 | .underline_thickness = ${f.src.underlineThickness}, 110 | #endif 111 | ${this.get_static_bitmap()} 112 | .dsc = &font_dsc, /*The custom font data. Will be accessed by \`get_glyph_bitmap/dsc\` */ 113 | #if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9 114 | .fallback = ${f.fallback}, 115 | #endif 116 | .user_data = NULL, 117 | }; 118 | `.trim(); 119 | } 120 | } 121 | 122 | 123 | module.exports = LvHead; 124 | -------------------------------------------------------------------------------- /lib/writers/lvgl/lv_table_kern.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const u = require('../../utils'); 5 | const Kern = require('../../font/table_kern'); 6 | 7 | 8 | class LvKern extends Kern { 9 | constructor(font) { 10 | super(font); 11 | } 12 | 13 | to_lv_format0() { 14 | const f = this.font; 15 | let kern_pairs = this.collect_format0_data(); 16 | 17 | return ` 18 | /*----------------- 19 | * KERNING 20 | *----------------*/ 21 | 22 | 23 | /*Pair left and right glyphs for kerning*/ 24 | static const ${f.glyphIdFormat ? 'uint16_t' : 'uint8_t'} kern_pair_glyph_ids[] = 25 | { 26 | ${kern_pairs.map(pair => ` ${pair[0]}, ${pair[1]}`).join(',\n')} 27 | }; 28 | 29 | /* Kerning between the respective left and right glyphs 30 | * 4.4 format which needs to scaled with \`kern_scale\`*/ 31 | static const int8_t kern_pair_values[] = 32 | { 33 | ${u.long_dump(kern_pairs.map(pair => f.kernToFP(pair[2])))} 34 | }; 35 | 36 | /*Collect the kern pair's data in one place*/ 37 | static const lv_font_fmt_txt_kern_pair_t kern_pairs = 38 | { 39 | .glyph_ids = kern_pair_glyph_ids, 40 | .values = kern_pair_values, 41 | .pair_cnt = ${kern_pairs.length}, 42 | .glyph_ids_size = ${f.glyphIdFormat} 43 | }; 44 | 45 | 46 | `.trim(); 47 | } 48 | 49 | to_lv_format3() { 50 | const f = this.font; 51 | const { 52 | left_classes, 53 | right_classes, 54 | left_mapping, 55 | right_mapping, 56 | values 57 | } = this.collect_format3_data(); 58 | 59 | return ` 60 | /*----------------- 61 | * KERNING 62 | *----------------*/ 63 | 64 | 65 | /*Map glyph_ids to kern left classes*/ 66 | static const uint8_t kern_left_class_mapping[] = 67 | { 68 | ${u.long_dump(left_mapping)} 69 | }; 70 | 71 | /*Map glyph_ids to kern right classes*/ 72 | static const uint8_t kern_right_class_mapping[] = 73 | { 74 | ${u.long_dump(right_mapping)} 75 | }; 76 | 77 | /*Kern values between classes*/ 78 | static const int8_t kern_class_values[] = 79 | { 80 | ${u.long_dump(values.map(v => f.kernToFP(v)))} 81 | }; 82 | 83 | 84 | /*Collect the kern class' data in one place*/ 85 | static const lv_font_fmt_txt_kern_classes_t kern_classes = 86 | { 87 | .class_pair_values = kern_class_values, 88 | .left_class_mapping = kern_left_class_mapping, 89 | .right_class_mapping = kern_right_class_mapping, 90 | .left_class_cnt = ${left_classes}, 91 | .right_class_cnt = ${right_classes}, 92 | }; 93 | 94 | 95 | `.trim(); 96 | } 97 | 98 | toLVGL() { 99 | const f = this.font; 100 | 101 | if (!f.hasKerning()) return ''; 102 | 103 | /* eslint-disable no-console */ 104 | 105 | if (f.kern.should_use_format3()) { 106 | if (f.kern.format3_forced) { 107 | let diff = this.create_format3_data().length - this.create_format0_data().length; 108 | console.log(`Forced faster kerning format (via classes). Size increase is ${diff} bytes.`); 109 | } 110 | return this.to_lv_format3(); 111 | } 112 | 113 | if (this.font.opts.fast_kerning) { 114 | console.log('Forced faster kerning format (via classes), but data exceeds it\'s limits. Continue use pairs.'); 115 | } 116 | return this.to_lv_format0(); 117 | } 118 | } 119 | 120 | 121 | module.exports = LvKern; 122 | -------------------------------------------------------------------------------- /lv_font_conv.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const AppError = require('./lib/app_error'); 6 | 7 | require('./lib/cli').run(process.argv.slice(2)).catch(err => { 8 | /*eslint-disable no-console*/ 9 | if (err instanceof AppError) { 10 | // Try to beautify normal errors 11 | console.error(err.message.trim()); 12 | } else { 13 | // Print crashes 14 | console.error(err.stack); 15 | } 16 | process.exit(1); 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lv_font_conv", 3 | "version": "1.5.3", 4 | "description": "Rasterize vector fonts for embedded use. Supports subsettings & merge.", 5 | "keywords": [ 6 | "font", 7 | "convertor", 8 | "embedded" 9 | ], 10 | "repository": "lvgl/lv_font_conv", 11 | "license": "MIT", 12 | "files": [ 13 | "lv_font_conv.js", 14 | "lib/" 15 | ], 16 | "bin": { 17 | "lv_font_conv": "lv_font_conv.js" 18 | }, 19 | "scripts": { 20 | "start": "parcel ./web/index.html --open", 21 | "build": "support/build_web.js && browserify web/index.js -o dist/index.js", 22 | "build:dockerimage": "docker build -t lv_font_conv_freetype ./support", 23 | "build:freetype": "docker run --rm -v $(pwd):/src/lv_font_conv -it lv_font_conv_freetype ./lv_font_conv/support/build.sh", 24 | "lint": "eslint .", 25 | "test": "npm run lint && nyc mocha --recursive", 26 | "coverage": "npm run test && nyc report --reporter html", 27 | "shrink-deps": "shx rm -rf node_modules/opentype.js/src node_modules/opentype.js/dist/opentype.{m,js.m}* node_modules/pngjs/browser.js", 28 | "prepublishOnly": "npm run shrink-deps" 29 | }, 30 | "dependencies": { 31 | "argparse": "^2.0.0", 32 | "bit-buffer": "^0.2.5", 33 | "debug": "^4.3.3", 34 | "make-error": "^1.3.5", 35 | "mkdirp": "^1.0.4", 36 | "opentype.js": "^1.3.4", 37 | "pngjs": "^6.0.0" 38 | }, 39 | "bundledDependencies": [ 40 | "argparse", 41 | "bit-buffer", 42 | "debug", 43 | "make-error", 44 | "mkdirp", 45 | "opentype.js", 46 | "pngjs" 47 | ], 48 | "devDependencies": { 49 | "browserify": "^17.0.0", 50 | "eslint": "^8.7.0", 51 | "file-saver": "^2.0.2", 52 | "mocha": "^9.1.4", 53 | "nyc": "^15.1.0", 54 | "roboto-fontface": "^0.10.0", 55 | "shelljs": "^0.8.5" 56 | }, 57 | "browserslist": [ 58 | "last 1 Chrome version" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /support/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM emscripten/emsdk:3.1.1 2 | 3 | RUN wget --no-check-certificate https://download.savannah.gnu.org/releases/freetype/freetype-2.11.1.tar.xz && \ 4 | tar xf freetype-2.11.1.tar.xz 5 | 6 | RUN apt-get -qq -y update && \ 7 | apt-get -qq install -y --no-install-recommends file 8 | 9 | RUN cd freetype-2.11.1 && \ 10 | gcc -o objs/apinames src/tools/apinames.c && \ 11 | mv ./modules.cfg ./modules.cfg.orig && \ 12 | grep -v -E "+= type1|+= cid|+= pfr|+= type42|+= winfonts|+= pcf|+= bdf|+= cache|+= gxvalid|+= lzw|+= bzip2|+= otvalid" modules.cfg.orig > modules.cfg && \ 13 | emconfigure ./configure CFLAGS='-Os -D FT_CONFIG_OPTION_SYSTEM_ZLIB -s USE_ZLIB=1 -DFT_CONFIG_OPTION_DISABLE_STREAM_SUPPORT' && \ 14 | emmake make && \ 15 | emmake make install 16 | -------------------------------------------------------------------------------- /support/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p /src/lv_font_conv/lib/freetype/build 4 | 5 | emcc --bind \ 6 | -o /src/lv_font_conv/lib/freetype/build/ft_render.js \ 7 | /src/lv_font_conv/lib/freetype/render.c \ 8 | -s USE_ZLIB=1 \ 9 | -L/usr/local/lib -lfreetype -I/usr/local/include/freetype2 \ 10 | -s "EXPORTED_FUNCTIONS=[\ 11 | '_FT_Init_FreeType',\ 12 | '_FT_Done_FreeType',\ 13 | '_FT_New_Memory_Face',\ 14 | '_FT_Done_Face',\ 15 | '_FT_Set_Char_Size',\ 16 | '_FT_Set_Pixel_Sizes',\ 17 | '_FT_Get_Char_Index',\ 18 | '_FT_Load_Glyph',\ 19 | '_FT_Render_Glyph',\ 20 | '_FT_Get_Kerning',\ 21 | '_FT_Get_Sfnt_Table',\ 22 | '_init_constants'\ 23 | ]"\ 24 | -s "EXPORTED_RUNTIME_METHODS=[\ 25 | 'ccall',\ 26 | 'cwrap',\ 27 | 'getValue',\ 28 | 'writeArrayToMemory'\ 29 | ]"\ 30 | -s MODULARIZE=1 \ 31 | -s NO_FILESYSTEM=1 \ 32 | -s SINGLE_FILE=1 \ 33 | -s NODEJS_CATCH_EXIT=0 \ 34 | -s NODEJS_CATCH_REJECTION=0 \ 35 | -s ALLOW_MEMORY_GROWTH=1 \ 36 | -Os 37 | -------------------------------------------------------------------------------- /support/build_web.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const shell = require('shelljs'); 6 | const fs = require('fs'); 7 | 8 | const OUT_DIR = 'dist'; 9 | 10 | shell.rm('-rf', OUT_DIR); 11 | shell.mkdir(OUT_DIR); 12 | 13 | const content = fs.readFileSync('web/content.html', 'utf8'); 14 | const index = fs.readFileSync('web/index.html', 'utf8'); 15 | 16 | fs.writeFileSync(`${OUT_DIR}/content.html`, content); 17 | 18 | const index_out = index.replace('', content); 19 | fs.writeFileSync(`${OUT_DIR}/index.html`, index_out); 20 | -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /test/font/fixtures/font_info_AV.json: -------------------------------------------------------------------------------- 1 | { 2 | "ascent": 9, 3 | "descent": -2, 4 | "typoAscent": 8, 5 | "typoDescent": -2, 6 | "typoLineGap": 0, 7 | "size": 10, 8 | "glyphs": [ 9 | { 10 | "code": 65, 11 | "advanceWidth": 6.5234375, 12 | "bbox": { 13 | "x": 0, 14 | "y": 0, 15 | "width": 7, 16 | "height": 8 17 | }, 18 | "kerning": { 19 | "86": -0.4248046875 20 | }, 21 | "pixels": [ 22 | [ 23 | 171, 24 | 70, 25 | 0, 26 | 0, 27 | 0, 28 | 188, 29 | 51 30 | ], 31 | [ 32 | 74, 33 | 153, 34 | 0, 35 | 0, 36 | 22, 37 | 207, 38 | 0 39 | ], 40 | [ 41 | 2, 42 | 226, 43 | 255, 44 | 255, 45 | 255, 46 | 111, 47 | 0 48 | ], 49 | [ 50 | 0, 51 | 133, 52 | 68, 53 | 0, 54 | 182, 55 | 21, 56 | 0 57 | ], 58 | [ 59 | 0, 60 | 34, 61 | 170, 62 | 35, 63 | 169, 64 | 0, 65 | 0 66 | ], 67 | [ 68 | 0, 69 | 0, 70 | 188, 71 | 166, 72 | 73, 73 | 0, 74 | 0 75 | ], 76 | [ 77 | 0, 78 | 0, 79 | 96, 80 | 228, 81 | 3, 82 | 0, 83 | 0 84 | ], 85 | [ 86 | 0, 87 | 0, 88 | 5, 89 | 23, 90 | 0, 91 | 0, 92 | 0 93 | ] 94 | ] 95 | }, 96 | { 97 | "code": 86, 98 | "advanceWidth": 6.3623046875, 99 | "bbox": { 100 | "x": 0, 101 | "y": 0, 102 | "width": 7, 103 | "height": 8 104 | }, 105 | "kerning": { 106 | "65": -0.3662109375 107 | }, 108 | "pixels": [ 109 | [ 110 | 0, 111 | 0, 112 | 107, 113 | 199, 114 | 0, 115 | 0, 116 | 0 117 | ], 118 | [ 119 | 0, 120 | 0, 121 | 198, 122 | 208, 123 | 39, 124 | 0, 125 | 0 126 | ], 127 | [ 128 | 0, 129 | 38, 130 | 188, 131 | 101, 132 | 133, 133 | 0, 134 | 0 135 | ], 136 | [ 137 | 0, 138 | 135, 139 | 101, 140 | 17, 141 | 221, 142 | 3, 143 | 0 144 | ], 145 | [ 146 | 2, 147 | 221, 148 | 19, 149 | 0, 150 | 179, 151 | 67, 152 | 0 153 | ], 154 | [ 155 | 68, 156 | 180, 157 | 0, 158 | 0, 159 | 90, 160 | 162, 161 | 0 162 | ], 163 | [ 164 | 162, 165 | 93, 166 | 0, 167 | 0, 168 | 11, 169 | 233, 170 | 13 171 | ], 172 | [ 173 | 29, 174 | 6, 175 | 0, 176 | 0, 177 | 0, 178 | 28, 179 | 7 180 | ] 181 | ] 182 | } 183 | ] 184 | } -------------------------------------------------------------------------------- /test/font/test_cmap_build_subtables.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const cmap_split = require('../../lib/font/cmap_build_subtables'); 6 | 7 | function range(from, to) { 8 | return Array(to - from + 1).fill(0).map((_, i) => from + i); 9 | } 10 | 11 | 12 | describe('Cmap build subtables', function () { 13 | 14 | it('Should represent a single character as format0', function () { 15 | assert.deepEqual(cmap_split([ 42 ]), [ [ 'format0_tiny', [ 42 ] ] ]); 16 | }); 17 | 18 | it('Should represent two characters as sparse', function () { 19 | assert.deepEqual(cmap_split([ 10, 100 ]), [ [ 'sparse_tiny', [ 10, 100 ] ] ]); 20 | }); 21 | 22 | it('Should split ranges', function () { 23 | assert.deepEqual(cmap_split([ 1, ...range(100, 140), 200 ]), [ 24 | [ 'format0_tiny', [ 1 ] ], 25 | [ 'format0_tiny', range(100, 140) ], 26 | [ 'format0_tiny', [ 200 ] ] 27 | ]); 28 | }); 29 | 30 | it('Should split more than 256 characters into multiple ranges', function () { 31 | assert.deepEqual(cmap_split(range(1, 257)), [ 32 | [ 'format0_tiny', [ 1 ] ], 33 | [ 'format0_tiny', range(2, 257) ] 34 | ]); 35 | }); 36 | 37 | it('Should split en+de+ru set optimally', function () { 38 | let set = [ 39 | ...range(65, 90), ...range(97, 122), // en + de 40 | 196, 214, 220, 223, 228, 246, 252, // de, umlauts and eszett 41 | 1025, ...range(1040, 1103), 1105, // ru 42 | 7838 // de, capital eszett 43 | ]; 44 | 45 | assert.deepEqual(cmap_split(set), [ 46 | [ 'format0_tiny', range(65, 90) ], 47 | [ 'format0_tiny', range(97, 122) ], 48 | [ 'sparse_tiny', [ 196, 214, 220, 223, 228, 246, 252, 1025 ] ], 49 | [ 'format0_tiny', range(1040, 1103) ], 50 | [ 'sparse_tiny', [ 1105, 7838 ] ] 51 | ]); 52 | }); 53 | 54 | it('Should split sparse set with >65535 gap', function () { 55 | let set = [ 56 | 1, 11, 21, 31, 41, 51, 61, 65531, 65541, 65551, 65561, 65571, 65581 57 | ]; 58 | 59 | assert.deepEqual(cmap_split(set), [ 60 | [ 'sparse_tiny', [ 1, 11, 21, 31, 41 ] ], 61 | [ 'sparse_tiny', [ 51, 61, 65531, 65541, 65551, 65561, 65571, 65581 ] ] 62 | ]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/font/test_compress.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const compress = require('../../lib/font/compress'); 6 | const { BitStream } = require('bit-buffer'); 7 | 8 | 9 | function c(data, opts) { 10 | const buf = Buffer.alloc(data.length * 2 + 100); 11 | const bs = new BitStream(buf); 12 | bs.bigEndian = true; 13 | compress(bs, data, opts); 14 | const result = Buffer.alloc(bs.byteIndex); 15 | buf.copy(result, 0, 0, bs.byteIndex); 16 | return result; 17 | } 18 | 19 | describe('Compress', function () { 20 | 21 | it('pass through, bpp=8', function () { 22 | assert.deepEqual( 23 | c([ 0x1, 0x2, 0x3, 0x2 ], { bpp: 8 }), 24 | Buffer.from([ 0x1, 0x2, 0x3, 0x2 ]) 25 | ); 26 | }); 27 | 28 | it('pass through, bpp=4', function () { 29 | assert.deepEqual( 30 | c([ 0x1, 0x2, 0x3, 0x2 ], { bpp: 4 }), 31 | // 0001 0010 0011 0010 32 | Buffer.from([ 0x12, 0x32 ]) 33 | ); 34 | }); 35 | 36 | it('pass through, bpp=3', function () { 37 | assert.deepEqual( 38 | c([ 0xFF, 0xF1, 0xFF ], { bpp: 3 }), 39 | // 111 001 11|1 0000000 40 | Buffer.from([ 0xE7, 0x80 ]) 41 | ); 42 | }); 43 | 44 | it('collapse to bit', function () { 45 | assert.deepEqual( 46 | c([ 0x1, 0x3, 0x3, 0x3, 0x1 ], { bpp: 4 }), 47 | // 0001 0011 | 0011 1 0 00|01 000000 48 | Buffer.from([ 0b00010011, 0b00111000, 0b01000000 ]) 49 | ); 50 | }); 51 | 52 | it('collapse 10+ repeats with counter', function () { 53 | const data = Array(15).fill(0); 54 | data[data.length - 1] = 0b11; 55 | assert.deepEqual( 56 | c(data, { bpp: 2 }), 57 | // 00 00 1111|1111111 0|00010 11 0 58 | Buffer.from([ 0b00001111, 0b11111110, 0b00010110 ]) 59 | ); 60 | }); 61 | 62 | it('split repeats if counter overflows', function () { 63 | const data = Array(77).fill(0); 64 | data[data.length - 1] = 3; 65 | assert.deepEqual( 66 | c(data, { bpp: 2 }), 67 | // 00 00 1111|1111111 1|11111 00 1|1 0000000 68 | Buffer.from([ 0b00001111, 0b11111111, 0b11111001, 0b10000000 ]) 69 | ); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /test/font/test_font.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const Font = require('../../lib/font/font'); 6 | const { BitStream } = require('bit-buffer'); 7 | 8 | /*eslint-disable max-len*/ 9 | 10 | // Regenerate: 11 | // ./lv_font_conv.js --font ./node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff -r 65-65 -r 86-86 --bpp 1 --size 10 --format dump -o 1111 --full-info 12 | const font_data_AV = require('./fixtures/font_info_AV.json'); 13 | // ./lv_font_conv.js --font ./node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff -r 65-65 -r 86-86 --bpp 1 --size 200 --format dump -o 1111 --full-info 14 | const font_data_AV_size200 = require('./fixtures/font_info_AV_size200.json'); 15 | const font_options = { bpp: 2 }; 16 | 17 | /*eslint-enable max-len*/ 18 | 19 | 20 | describe('Font', function () { 21 | 22 | it('head table', function () { 23 | let font = new Font(font_data_AV, font_options); 24 | let bin = font.head.toBin(); 25 | 26 | assert.equal(bin.readUInt32LE(0), bin.length); 27 | assert.equal(bin.length % 4, 0); 28 | 29 | // Make sure name chars order is proper 30 | assert.equal(bin.readUInt8(4), 'h'.charCodeAt(0)); 31 | assert.equal(bin.readUInt8(5), 'e'.charCodeAt(0)); 32 | assert.equal(bin.readUInt8(6), 'a'.charCodeAt(0)); 33 | assert.equal(bin.readUInt8(7), 'd'.charCodeAt(0)); 34 | 35 | assert.equal(bin.readUInt32LE(8), 1); // version 36 | assert.equal(bin.readUInt16LE(12), 4); // amount of next tables 37 | assert.equal(bin.readUInt16LE(14), font_data_AV.size); 38 | assert.equal(bin.readUInt16LE(16), font_data_AV.ascent); 39 | assert.equal(bin.readInt16LE(18), font_data_AV.descent); 40 | assert.equal(bin.readUInt16LE(20), font_data_AV.typoAscent); 41 | assert.equal(bin.readInt16LE(22), font_data_AV.typoDescent); 42 | assert.equal(bin.readUInt16LE(24), font_data_AV.typoLineGap); 43 | 44 | assert.equal(bin.readInt16LE(26), 0); // minY 45 | assert.equal(bin.readInt16LE(28), 8); // maxY 46 | 47 | // Default advanceWidth 0 for proportional fonts 48 | assert.equal(bin.readUInt16LE(30), 0); 49 | 50 | assert.equal(bin.readUInt16LE(32), Math.round(font.kerningScale * 16)); 51 | 52 | assert.equal(bin.readUInt8(34), 0); // indexToLocFormat 53 | assert.equal(bin.readUInt8(35), 0); // glyphIdFormat 54 | 55 | 56 | assert.equal(bin.readUInt8(36), 1); // advanceWidthFormat (with fractional) 57 | 58 | assert.equal(bin.readUInt8(37), font_options.bpp); 59 | 60 | assert.equal(bin.readUInt8(38), 1); // xy_bits 61 | assert.equal(bin.readUInt8(39), 4); // wh_bits 62 | assert.equal(bin.readUInt8(40), 8); // advanceWidth bits (FP4.4) 63 | 64 | assert.equal(bin.readUInt8(41), 1); // compression id 65 | 66 | assert.equal(bin.readUInt8(42), 0); // no subpixels 67 | }); 68 | 69 | 70 | it('loca table', function () { 71 | let font = new Font(font_data_AV, font_options); 72 | let bin = font.loca.toBin(); 73 | 74 | assert.equal(bin.readUInt16LE(0), bin.length); 75 | assert.equal(bin.length % 4, 0); 76 | assert.equal(bin.readUInt32LE(4), Buffer.from('loca').readUInt32LE(0)); 77 | 78 | // Entries (2 chars + reserved 'zero') 79 | assert.equal(bin.readUInt32LE(8), 3); 80 | 81 | // Check glyph data offsets 82 | // Offset = 12 is for `zero`, start check from 14 83 | assert.equal(bin.readUInt16LE(14), font.glyf.getOffset(1)); // for "A" 84 | assert.equal(bin.readUInt16LE(14), 8); 85 | assert.equal(bin.readUInt16LE(16), font.glyf.getOffset(2)); // for "W" 86 | assert.equal(bin.readUInt16LE(16), 25); 87 | }); 88 | 89 | 90 | it('glyf table', function () { 91 | let font = new Font(font_data_AV, font_options); 92 | let bin = font.glyf.toBin(); 93 | 94 | assert.equal(bin.readUInt16LE(0), bin.length); 95 | assert.equal(bin.length % 4, 0); 96 | assert.equal(bin.readUInt32LE(4), Buffer.from('glyf').readUInt32LE(0)); 97 | 98 | // Test 'V' glyph properties (ID = 2) 99 | 100 | // Extract data 101 | const bits = Buffer.alloc(bin.length - font.glyf.getOffset(2)); 102 | bin.copy(bits, 0, font.glyf.getOffset(2)); 103 | // Create bits loader 104 | const bs = new BitStream(bits); 105 | bs.bigEndian = true; 106 | 107 | assert.equal( 108 | bs.readBits(font.advanceWidthBits, false), 109 | Math.round(font_data_AV.glyphs[1].advanceWidth * 16) 110 | ); 111 | assert.equal(bs.readBits(font.xy_bits, true), font_data_AV.glyphs[1].bbox.x); 112 | assert.equal(bs.readBits(font.xy_bits, true), font_data_AV.glyphs[1].bbox.y); 113 | assert.equal(bs.readBits(font.wh_bits, false), font_data_AV.glyphs[1].bbox.width); 114 | assert.equal(bs.readBits(font.wh_bits, false), font_data_AV.glyphs[1].bbox.height); 115 | }); 116 | 117 | 118 | it('cmap table', function () { 119 | let font = new Font(font_data_AV, font_options); 120 | let bin = font.cmap.toBin(); 121 | 122 | assert.equal(bin.readUInt16LE(0), bin.length); 123 | assert.equal(bin.length % 4, 0); 124 | assert.equal(bin.readUInt32LE(4), Buffer.from('cmap').readUInt32LE(0)); 125 | 126 | assert.equal(bin.readUInt32LE(8), 1); // subtables count 127 | 128 | const SUB1_HEAD_OFFSET = 12; 129 | const SUB1_DATA_OFFSET = 12 + 16; 130 | 131 | // Check subtable header 132 | assert.equal(bin.readUInt32LE(SUB1_HEAD_OFFSET + 0), SUB1_DATA_OFFSET); 133 | assert.equal(bin.readUInt32LE(SUB1_HEAD_OFFSET + 4), 65); // "A" 134 | assert.equal(bin.readUInt16LE(SUB1_HEAD_OFFSET + 8), 22); // Range length, 86-65+1 135 | assert.equal(bin.readUInt16LE(SUB1_HEAD_OFFSET + 10), 1); // Glyph ID offset 136 | assert.equal(bin.readUInt16LE(SUB1_HEAD_OFFSET + 12), 2); // Entries count 137 | //assert.equal(bin.readUInt8(SUB1_HEAD_OFFSET + 14), 1); // Subtable type 138 | assert.equal(bin.readUInt8(SUB1_HEAD_OFFSET + 14), 3); // Subtable type 139 | 140 | // Check IDs (sparse subtable) 141 | assert.equal(bin.readUInt16LE(SUB1_DATA_OFFSET + 0), 0); // 'A' => 65+0 => 65 142 | //assert.equal(bin.readUInt16LE(SUB1_DATA_OFFSET + 4), 0); // 'A' ID => 1+0 => 1 143 | assert.equal(bin.readUInt16LE(SUB1_DATA_OFFSET + 2), 21); // 'W' => 65+21 => 86 144 | //assert.equal(bin.readUInt16LE(SUB1_DATA_OFFSET + 6), 1); // 'W' ID => 1+1 => 2 145 | }); 146 | 147 | 148 | describe('kern table', function () { 149 | it('header', function () { 150 | let font = new Font(font_data_AV, font_options); 151 | let bin = font.kern.toBin(); 152 | 153 | assert.equal(bin.readUInt16LE(0), bin.length); 154 | assert.equal(bin.length % 4, 0); 155 | assert.equal(bin.readUInt32LE(4), Buffer.from('kern').readUInt32LE(0)); 156 | assert.equal(bin.readUInt8(8), 0); // format 157 | }); 158 | 159 | it('sub format 0', function () { 160 | let font = new Font(font_data_AV, font_options); 161 | let bin = font.kern.toBin(); 162 | 163 | // Entries 164 | assert.equal(bin.readUInt32LE(12), 2); 165 | 166 | const PAIRS_OFFSET = 16; 167 | const VAL_OFFSET = PAIRS_OFFSET + bin.readUInt32LE(12) * 2; 168 | 169 | // Pairs of IDs 170 | 171 | // [ AV ] => [ 1, 2 ] 172 | assert.equal(bin.readUInt8(PAIRS_OFFSET + 0), 1); 173 | assert.equal(bin.readUInt8(PAIRS_OFFSET + 1), 2); 174 | // [ VA ] => [ 2, 1 ] 175 | assert.equal(bin.readUInt8(PAIRS_OFFSET + 2), 2); 176 | assert.equal(bin.readUInt8(PAIRS_OFFSET + 3), 1); 177 | 178 | // Values 179 | const AV_KERN_FP4 = Math.round(font_data_AV.glyphs[0].kerning['V'.charCodeAt(0)] * 16); 180 | assert.equal(bin.readInt8(VAL_OFFSET + 0), AV_KERN_FP4); 181 | const VA_KERN_FP4 = Math.round(font_data_AV.glyphs[1].kerning['A'.charCodeAt(0)] * 16); 182 | assert.equal(bin.readInt8(VAL_OFFSET + 1), VA_KERN_FP4); 183 | }); 184 | 185 | 186 | it('kerning values scale', function () { 187 | function isSimilar(a, b, epsilon) { 188 | return Math.abs(a - b) < epsilon; 189 | } 190 | 191 | const font = new Font(font_data_AV_size200, font_options); 192 | const bin_all = font.toBin(); 193 | const bin_kern = font.kern.toBin(); 194 | 195 | const kScale_FP4 = bin_all.readUInt16LE(32); 196 | 197 | const PAIRS_OFFSET = 16; 198 | const VAL_OFFSET = PAIRS_OFFSET + bin_kern.readUInt32LE(12) * 2; 199 | 200 | const AV_KERN = font_data_AV_size200.glyphs[0].kerning['V'.charCodeAt(0)]; 201 | assert.ok(isSimilar( 202 | ((bin_kern.readInt8(VAL_OFFSET + 0) * kScale_FP4) >> 4) / 16, 203 | AV_KERN, 204 | 0.1 205 | )); 206 | const VA_KERN = font_data_AV_size200.glyphs[1].kerning['A'.charCodeAt(0)]; 207 | assert.ok(isSimilar( 208 | ((bin_kern.readInt8(VAL_OFFSET + 1) * kScale_FP4) >> 4) / 16, 209 | VA_KERN, 210 | 0.1 211 | )); 212 | }); 213 | 214 | 215 | it('sub format 3', function () { 216 | let font = new Font(font_data_AV, font_options); 217 | let bin_sub3 = font.kern.create_format3_data(); 218 | 219 | const map_len = bin_sub3.readUInt16LE(0); 220 | assert.equal(map_len, 3); 221 | const left_len = bin_sub3.readUInt8(2); 222 | assert.equal(left_len, 2); 223 | const right_len = bin_sub3.readUInt8(3); 224 | assert.equal(right_len, 2); 225 | 226 | const offs_map_left = 4; 227 | const offs_map_right = 4 + map_len; 228 | const offs_k_array = 4 + 2 * map_len; 229 | 230 | // Values 231 | const AV_KERN_FP4 = Math.round(font_data_AV.glyphs[0].kerning['V'.charCodeAt(0)] * 16); 232 | const A_left = bin_sub3.readUInt8(font.glyph_id['A'.charCodeAt(0)] + offs_map_left) - 1; 233 | const V_right = bin_sub3.readUInt8(font.glyph_id['V'.charCodeAt(0)] + offs_map_right) - 1; 234 | assert.equal( 235 | bin_sub3.readInt8(A_left * right_len + V_right + offs_k_array), 236 | AV_KERN_FP4 237 | ); 238 | 239 | const VA_KERN_FP4 = Math.round(font_data_AV.glyphs[1].kerning['A'.charCodeAt(0)] * 16); 240 | const V_left = bin_sub3.readUInt8(font.glyph_id['V'.charCodeAt(0)] + offs_map_left) - 1; 241 | const A_right = bin_sub3.readUInt8(font.glyph_id['A'.charCodeAt(0)] + offs_map_right) - 1; 242 | assert.equal( 243 | bin_sub3.readInt8(V_left * right_len + A_right + offs_k_array), 244 | VA_KERN_FP4 245 | ); 246 | }); 247 | }); 248 | }); 249 | -------------------------------------------------------------------------------- /test/test_cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const { execFileSync } = require('child_process'); 6 | const fs = require('fs'); 7 | const path = require('path'); 8 | const rimraf = require('rimraf'); 9 | const run = require('../lib/cli').run; 10 | const range = require('../lib/cli')._range; 11 | 12 | 13 | const script_path = path.join(__dirname, '../lv_font_conv.js'); 14 | const font = require.resolve('roboto-fontface/fonts/roboto/Roboto-Black.woff'); 15 | 16 | 17 | describe('Cli', function () { 18 | 19 | it('Should run', function () { 20 | let out = execFileSync(script_path, [], { stdio: 'pipe' }); 21 | assert.equal(out.toString().substring(0, 5), 'usage'); 22 | }); 23 | 24 | 25 | it('Should print error if range is specified without font', async function () { 26 | await assert.rejects( 27 | run('--range 123 --font test'.split(' '), true), 28 | /Only allowed after/ 29 | ); 30 | }); 31 | 32 | 33 | it('Should print error if range is invalid', async function () { 34 | await assert.rejects( 35 | run('--font test --range invalid'.split(' '), true), 36 | /argument -r\/--range: invalid range value: 'invalid'/ 37 | ); 38 | }); 39 | 40 | 41 | it('Should require character set specified for each font', async function () { 42 | await assert.rejects( 43 | run('--font test --size 18 --bpp 4 --format dump'.split(' '), true), 44 | /You need to specify either / 45 | ); 46 | }); 47 | 48 | 49 | it('Should print error if size is invalid', async function () { 50 | await assert.rejects( 51 | run('--size 10xxx'.split(' '), true), 52 | /argument --size: invalid positive_int value: '10xxx'/ 53 | ); 54 | }); 55 | 56 | 57 | it('Should print error if size is zero', async function () { 58 | await assert.rejects( 59 | run('--size 0'.split(' '), true), 60 | /argument --size: invalid positive_int value: '0'/ 61 | ); 62 | }); 63 | 64 | 65 | it('Should write a font using "dump" writer', async function () { 66 | let rnd = Math.random().toString(16).slice(2, 10); 67 | let dir = path.join(__dirname, rnd); 68 | 69 | try { 70 | await run([ 71 | '--font', font, '--range', '0x20-0x22', '--size', '18', 72 | '-o', dir, '--bpp', '2', '--format', 'dump' 73 | ], true); 74 | 75 | assert.deepEqual(fs.readdirSync(dir), [ '20.png', '21.png', '22.png', 'font_info.json' ]); 76 | } finally { 77 | rimraf.sync(dir); 78 | } 79 | }); 80 | 81 | 82 | it('Should write a font using "bin" writer', async function () { 83 | let rnd = Math.random().toString(16).slice(2, 10) + '.font'; 84 | let file = path.join(__dirname, rnd); 85 | 86 | try { 87 | await run([ 88 | '--font', font, '--range', '0x20-0x22', '--size', '18', 89 | '-o', file, '--bpp', '2', '--format', 'bin' 90 | ], true); 91 | 92 | let contents = fs.readFileSync(file); 93 | 94 | assert.equal(contents.slice(4, 8), 'head'); 95 | } finally { 96 | fs.unlinkSync(file); 97 | } 98 | }); 99 | 100 | 101 | it('Should require output for "dump" writer', async function () { 102 | await assert.rejects( 103 | run([ '--font', font, '--range', '0x20-0x22', '--size', '18', '--bpp', '2', '--format', 'dump' ], true), 104 | /Output is required for/ 105 | ); 106 | }); 107 | 108 | describe('range', function () { 109 | it('Should accept single number', function () { 110 | assert.deepEqual(range('42'), [ 42, 42, 42 ]); 111 | }); 112 | 113 | it('Should accept single number (hex)', function () { 114 | assert.deepEqual(range('0x2A'), [ 42, 42, 42 ]); 115 | }); 116 | 117 | it('Should accept simple range', function () { 118 | assert.deepEqual(range('40-0x2A'), [ 40, 42, 40 ]); 119 | }); 120 | 121 | it('Should accept single number with mapping', function () { 122 | assert.deepEqual(range('42=>72'), [ 42, 42, 72 ]); 123 | }); 124 | 125 | it('Should accept range with mapping', function () { 126 | assert.deepEqual(range('42-45=>0x48'), [ 42, 45, 72 ]); 127 | }); 128 | 129 | it('Should error on invalid ranges', function () { 130 | assert.throws( 131 | () => range('20-19'), 132 | /Invalid range/ 133 | ); 134 | }); 135 | 136 | it('Should error on invalid numbers', function () { 137 | assert.throws( 138 | () => range('13-abc80'), 139 | /not a number/ 140 | ); 141 | }); 142 | 143 | it('Should not accept characters out of unicode range', function () { 144 | assert.throws( 145 | () => range('1114444'), 146 | /out of unicode/ 147 | ); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/test_collect_font_data.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const collect_font_data = require('../lib/collect_font_data'); 6 | const fs = require('fs'); 7 | 8 | 9 | const source_path = require.resolve('roboto-fontface/fonts/roboto/Roboto-Black.woff'); 10 | const source_bin = fs.readFileSync(source_path); 11 | 12 | 13 | describe('Collect font data', function () { 14 | 15 | it('Should convert range to bitmap', async function () { 16 | let out = await collect_font_data({ 17 | font: [ { 18 | source_path, 19 | source_bin, 20 | ranges: [ { range: [ 0x41, 0x42, 0x80 ] } ] 21 | } ], 22 | size: 18 23 | }); 24 | 25 | assert.equal(out.glyphs.length, 2); 26 | assert.equal(out.glyphs[0].code, 0x80); 27 | assert.equal(out.glyphs[1].code, 0x81); 28 | }); 29 | 30 | 31 | it('Should convert symbols to bitmap', async function () { 32 | let out = await collect_font_data({ 33 | font: [ { 34 | source_path, 35 | source_bin, 36 | ranges: [ { symbols: 'AB' } ] 37 | } ], 38 | size: 18 39 | }); 40 | 41 | assert.equal(out.glyphs.length, 2); 42 | assert.equal(out.glyphs[0].code, 0x41); 43 | assert.equal(out.glyphs[1].code, 0x42); 44 | }); 45 | 46 | 47 | it('Should not fail on combining characters', async function () { 48 | let out = await collect_font_data({ 49 | font: [ { 50 | source_path, 51 | source_bin, 52 | ranges: [ { range: [ 0x300, 0x300, 0x300 ] } ] 53 | } ], 54 | size: 18 55 | }); 56 | 57 | assert.equal(out.glyphs.length, 1); 58 | assert.equal(out.glyphs[0].code, 0x300); 59 | assert.strictEqual(out.glyphs[0].advanceWidth, 0); 60 | }); 61 | 62 | 63 | it('Should allow specifying same font multiple times', async function () { 64 | let out = await collect_font_data({ 65 | font: [ { 66 | source_path, 67 | source_bin, 68 | ranges: [ { range: [ 0x41, 0x41, 0x41 ] } ] 69 | }, { 70 | source_path, 71 | source_bin, 72 | ranges: [ { range: [ 0x51, 0x51, 0x51 ] } ] 73 | } ], 74 | size: 18 75 | }); 76 | 77 | assert.equal(out.glyphs.length, 2); 78 | }); 79 | 80 | 81 | it('Should allow multiple ranges', async function () { 82 | let out = await collect_font_data({ 83 | font: [ { 84 | source_path, 85 | source_bin, 86 | ranges: [ { range: [ 0x41, 0x41, 0x41, 0x51, 0x52, 0x51 ] } ] 87 | } ], 88 | size: 18 89 | }); 90 | 91 | assert.equal(out.glyphs.length, 3); 92 | }); 93 | 94 | 95 | 96 | it('Should work with sparse ranges', async function () { 97 | let out = await collect_font_data({ 98 | font: [ { 99 | source_path, 100 | source_bin, 101 | ranges: [ { range: [ 0x3d0, 0x3d8, 0x3d0 ] } ] 102 | } ], 103 | size: 10 104 | }); 105 | 106 | assert.equal(out.glyphs.length, 3); 107 | assert.equal(out.glyphs[0].code, 0x3d1); 108 | assert.equal(out.glyphs[1].code, 0x3d2); 109 | assert.equal(out.glyphs[2].code, 0x3d6); 110 | }); 111 | 112 | 113 | it('Should read kerning values', async function () { 114 | let out = await collect_font_data({ 115 | font: [ { 116 | source_path, 117 | source_bin, 118 | ranges: [ // AVW 119 | { range: [ 0x41, 0x41, 1 ] }, 120 | { range: [ 0x56, 0x57, 2 ] } 121 | ] 122 | } ], 123 | size: 18 124 | }); 125 | 126 | assert.equal(out.glyphs.length, 3); 127 | 128 | // A 129 | assert.equal(out.glyphs[0].code, 1); 130 | assert(out.glyphs[0].kerning[1] === undefined); 131 | assert(out.glyphs[0].kerning[2] < 0); 132 | assert(out.glyphs[0].kerning[3] < 0); 133 | 134 | // V 135 | assert.equal(out.glyphs[1].code, 2); 136 | assert(out.glyphs[1].kerning[1] < 0); 137 | assert(out.glyphs[1].kerning[2] === undefined); 138 | assert(out.glyphs[1].kerning[3] === undefined); 139 | 140 | // W 141 | assert.equal(out.glyphs[2].code, 3); 142 | assert(out.glyphs[2].kerning[1] < 0); 143 | assert(out.glyphs[2].kerning[2] === undefined); 144 | assert(out.glyphs[2].kerning[3] === undefined); 145 | }); 146 | 147 | 148 | it('Should error on empty ranges', async function () { 149 | await assert.rejects( 150 | collect_font_data({ 151 | font: [ { 152 | source_path, 153 | source_bin, 154 | ranges: [ { range: [ 0x3d3, 0x3d5, 0x3d3 ] } ] 155 | } ], 156 | size: 18 157 | }), 158 | /doesn't have any characters/ 159 | ); 160 | }); 161 | 162 | 163 | it('Should error on empty symbol sets', async function () { 164 | await assert.rejects( 165 | collect_font_data({ 166 | font: [ { 167 | source_path, 168 | source_bin, 169 | ranges: [ { symbols: '\u03d3\u03d4\u03d5' } ] 170 | } ], 171 | size: 18 172 | }), 173 | /doesn't have any characters/ 174 | ); 175 | }); 176 | 177 | 178 | it('Should error when font format is unknown', async function () { 179 | await assert.rejects( 180 | collect_font_data({ 181 | font: [ { 182 | source_path: __filename, 183 | source_bin: fs.readFileSync(__filename), 184 | ranges: [ { range: [ 0x20, 0x20, 0x20 ] } ] 185 | } ], 186 | size: 18 187 | }), 188 | /Cannot load font.*(Unknown|Unsupported)/ 189 | ); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /test/test_ranger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const Ranger = require('../lib/ranger'); 6 | 7 | 8 | describe('Ranger', function () { 9 | 10 | it('Should accept symbols', function () { 11 | let ranger = new Ranger(); 12 | assert.equal(ranger.add_symbols('font', 'aba8').length, 4); 13 | assert.deepEqual(ranger.get(), { 56: { font: 'font', code: 56 }, 14 | 97: { font: 'font', code: 97 }, 98: { font: 'font', code: 98 } }); 15 | }); 16 | 17 | it('Should handle astral characters correctly', function () { 18 | let ranger = new Ranger(); 19 | assert.equal(ranger.add_symbols('font', 'a𐌀b𐌁').length, 4); 20 | assert.deepEqual(ranger.get(), { 97: { font: 'font', code: 97 }, 98: { font: 'font', code: 98 }, 21 | 66304: { font: 'font', code: 66304 }, 66305: { font: 'font', code: 66305 } }); 22 | }); 23 | 24 | it('Should merge ranges', function () { 25 | let ranger = new Ranger(); 26 | assert.equal(ranger.add_range('font', 42, 44, 42).length, 3); 27 | assert.equal(ranger.add_range('font2', 46, 46, 85).length, 1); 28 | assert.deepEqual(ranger.get(), { 42: { font: 'font', code: 42 }, 43: { font: 'font', code: 43 }, 29 | 44: { font: 'font', code: 44 }, 85: { font: 'font2', code: 46 } }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/test_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const assert = require('assert'); 5 | const set_depth = require('../lib/utils').set_depth; 6 | 7 | 8 | describe('Utils', function () { 9 | 10 | describe('set_depth', function () { 11 | it('Should reduce glyph to depth=1', function () { 12 | let input = [ 0b00000000, 0b01111111, 0b10000000, 0b11111111 ]; 13 | let expect = [ 0b00000000, 0b00000000, 0b11111111, 0b11111111 ]; 14 | let depth = 1; 15 | 16 | let glyph = set_depth({ 17 | bbox: { x: 0, y: 0, width: input.length, height: 1 }, 18 | pixels: [ input ] 19 | }, depth); 20 | 21 | assert.deepEqual(glyph.pixels[0], expect); 22 | }); 23 | 24 | it('Should reduce glyph to depth=2', function () { 25 | let input = [ 0b00111111, 0b01000000, 0b10111111, 0b11000000 ]; 26 | let expect = [ 0b00000000, 0b01010101, 0b10101010, 0b11111111 ]; 27 | let depth = 2; 28 | 29 | let glyph = set_depth({ 30 | bbox: { x: 0, y: 0, width: input.length, height: 1 }, 31 | pixels: [ input ] 32 | }, depth); 33 | 34 | assert.deepEqual(glyph.pixels[0], expect); 35 | }); 36 | 37 | it('Should reduce glyph to depth=3', function () { 38 | let input = [ 0b00111111, 0b01000000, 0b01011111, 0b01100000, 0b01111111, 39 | 0b10000000, 0b10011111, 0b10100000, 0b10111111, 0b11000000 ]; 40 | let expect = [ 0b00100100, 0b01001001, 0b01001001, 0b01101101, 0b01101101, 41 | 0b10010010, 0b10010010, 0b10110110, 0b10110110, 0b11011011 ]; 42 | let depth = 3; 43 | 44 | let glyph = set_depth({ 45 | bbox: { x: 0, y: 0, width: input.length, height: 1 }, 46 | pixels: [ input ] 47 | }, depth); 48 | 49 | assert.deepEqual(glyph.pixels[0], expect); 50 | }); 51 | 52 | it('Should reduce glyph to depth=8', function () { 53 | let input = [ 0b11001001, 0b00001111, 0b11011010, 0b10100010 ]; 54 | let expect = [ 0b11001001, 0b00001111, 0b11011010, 0b10100010 ]; 55 | let depth = 8; 56 | 57 | let glyph = set_depth({ 58 | bbox: { x: 0, y: 0, width: input.length, height: 1 }, 59 | pixels: [ input ] 60 | }, depth); 61 | 62 | assert.deepEqual(glyph.pixels[0], expect); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /web/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | parserOptions: 4 | sourceType: module 5 | -------------------------------------------------------------------------------- /web/.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "doctype-first": false 3 | } -------------------------------------------------------------------------------- /web/content.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

With this free online font converter tool you can create C array from any TTF or WOFF font. You can select ranges of Unicode characters and specify the bpp (bit-per-pixel).

4 | 5 |

The font converter is designed to be compatible with LVGL but with minor modification you can make it compatible with other graphics libraries.

6 | 7 |

The offline version of the converter (as well as the source code for this website) is available here

8 | 9 |

How to use the font converter?

10 |
    11 |
  1. Give name to the output font. E.g. "arial_40"
  2. 12 |
  3. Specify the height in px
  4. 13 |
  5. Set the bpp (bit-per-piel). Higher value results smoother (anti-aliased) font
  6. 14 |
  7. Choose a TTF or WOFF font
  8. 15 |
  9. Set a range of Unicode character to include in your font or list the characters in the Symbols field
  10. 16 |
  11. Optionally choose another font too and specify the ranges and/or symbols for it as well. The characters will be merged into the final C file.
  12. 17 |
  13. Click the Convert button to download the result C file.
  14. 18 |
19 | 20 |

How to use the generated fonts in LVGL?

21 |
    22 |
  1. Copy the result C file into your LVGL project
  2. 23 |
  3. In a C file of your application declare the font as: extern const lv_font_t my_font_name; 24 | or simply LV_FONT_DECLARE(my_font_name);
  4. 25 |
  5. Set the font in a style: style.text.font = &my_font_name;
  6. 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |
44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 |
53 | 54 |
55 |
56 | 57 |
58 | 59 |
60 | 67 |
68 |
69 | 70 |
71 | 72 |
73 | 74 | 77 |
78 | 79 |
80 | 81 | 82 | 88 |
89 | 90 | 93 |
94 | 100 | 101 |
102 | 103 |
104 | 105 | 109 |
110 |
111 | 112 | 113 |
114 |
115 | 116 |
117 | 118 |
119 |
120 | 121 |
122 | 123 |
124 | 125 |
126 |
127 |
128 | 129 |
130 | 131 |
132 |
133 |
134 |
135 | 136 | 137 |
138 | You can use both "Range" and "Symbols" or only one of them 139 |
140 | 141 |
142 | 143 |
144 | 145 |
146 |
147 |
148 | 149 |

Old version for v5.3

150 | 151 |

Useful notes

152 |
    153 |
  • Unicode table to pick letters: https://symbl.cc/
  • 154 |
  • Unicode ranges http://jrgraphix.net/research/unicode.php
  • 155 |
  • A pixel perfect font: Terminus.
  • 156 |
  • Examples for two-tone or colored fonts: Material Design Icons (two-tone version), 157 | BungeeColor-Regular
  • 158 |
  • List of built-in symbols Use this FontAwesome symbol font and copy this list to Range:
    159 | 61441,61448,61451,61452,61452,61453,61457,61459,61461,61465,
    160 | 61468,61473,61478,61479,61480,61502,61512,61515,61516,61517,
    161 | 61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,
    162 | 61559,61560,61561,61563,61587,61589,61636,61637,61639,61671,
    163 | 61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,
    164 | 62020,62087,62099,62212,62189,62810,63426,63650 165 |
  • 166 |
  • To learn more about the font handling of LVGL read this Guide
  • 167 |
  • To use the Fonts without LVGL you need lv_font.c/h, lv_font_fmt_txt.c/h from here.
  • 168 |
169 |
170 | 171 | 172 | 173 |
174 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LVGL font converter 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /web/index.js: -------------------------------------------------------------------------------- 1 | const convert = require('../lib/convert'); 2 | const FileSaver = require('file-saver'); 3 | 4 | /*eslint-env jquery*/ 5 | 6 | 7 | function handleFiles() { 8 | const fileList = this.files; 9 | 10 | const $fontControls = $(this).closest('.font-controls'); 11 | 12 | if (!fileList.length) { 13 | $fontControls.data('selected-file', null); 14 | return; 15 | } 16 | 17 | const file = fileList[0]; 18 | 19 | const reader = new FileReader(); 20 | reader.onload = e => { 21 | $(this).closest('.font-controls').data('selected-file', { 22 | name: file.name, 23 | size: file.size, 24 | type: file.type, 25 | data: e.target.result 26 | }); 27 | }; 28 | reader.readAsArrayBuffer(file); 29 | } 30 | 31 | 32 | function addFontFileChangeHandlers() { 33 | $('.font_file').off('change', handleFiles); 34 | $('.font_file').on('change', handleFiles); 35 | } 36 | 37 | function resetInput($e) { 38 | $e.wrap('
').closest('form').get(0).reset(); 39 | $e.unwrap(); 40 | } 41 | 42 | function addFont() { 43 | let $clone = $('#font-controls-clone-source').clone(false); 44 | resetInput($clone.find('.font_file')); 45 | resetInput($clone.find('.font_range')); 46 | resetInput($clone.find('.font_symbols')); 47 | $clone.data('selected-file', null); /* just to be sure */ 48 | $clone.insertBefore($('#insert-button')); 49 | $('').addClass('btn btn-primary btn-md').text('Remove this font').click(function () { 50 | $(this).parent().remove(); 51 | }).insertBefore($clone.find('hr')); 52 | addFontFileChangeHandlers(); 53 | } 54 | 55 | $('#insert-button').click(addFont); 56 | 57 | addFontFileChangeHandlers(); 58 | 59 | function generate_range(start, end, transpose) { 60 | if (start !== transpose) { 61 | return start + '-' + end + '=>' + transpose; 62 | } 63 | if (start !== end) { 64 | return start + '-' + end; 65 | } 66 | return start; 67 | } 68 | 69 | function generate_opts_string(args) { 70 | var opts = []; 71 | 72 | opts.push('--bpp', args.bpp); 73 | opts.push('--size', args.size); 74 | if (args.no_compress) { 75 | opts.push('--no-compress'); 76 | } 77 | if (args.lcd) { 78 | opts.push('--lcd'); 79 | } 80 | if (args.lcd_v) { 81 | opts.push('--lcd-v'); 82 | } 83 | if (args.use_color_info) { 84 | opts.push('--use-color-info'); 85 | } 86 | 87 | for (var i = 0; i < args.font.length; i++) { 88 | opts.push('--font', args.font[i].source_path); 89 | const r = args.font[i].ranges; 90 | 91 | var symbols = ''; 92 | var ranges = []; 93 | for (var j = 0; j < r.length; j++) { 94 | if (r[j].symbols) { 95 | symbols += r[j].symbols; 96 | } 97 | for (var k = 0; k < r[j].range.length; k += 3) { 98 | ranges.push(generate_range(r[j].range[k + 0], r[j].range[k + 1], r[j].range[k + 2])); 99 | } 100 | } 101 | if (symbols) { 102 | opts.push('--symbols', symbols); 103 | } 104 | if (ranges.length !== 0) { 105 | opts.push('--range', ranges.join(',')); 106 | } 107 | } 108 | 109 | opts.push('--format', args.format); 110 | opts.push('-o', args.output + '.c'); 111 | 112 | return opts.join(' '); 113 | } 114 | 115 | document.querySelector('#converterForm').addEventListener('submit', function handleSubmit(e) { 116 | e.preventDefault(); 117 | 118 | var _name = document.getElementById('name').value; 119 | var _fallback = document.getElementById('fallback').value; 120 | var _size = document.getElementById('height').value; 121 | var _bpp = document.getElementById('bpp').value; 122 | /* eslint-disable max-depth, radix */ 123 | let fcnt = 0; 124 | let fonts = []; 125 | let r_str; 126 | let syms; 127 | $('.font-controls').each(function (index, el) { 128 | let $fontControls = $(el); 129 | r_str = $fontControls.find('.font_range').val(); 130 | syms = $fontControls.find('.font_symbols').val(); 131 | let selectedFile = $fontControls.data('selected-file'); 132 | if (selectedFile && (r_str.length || syms.length)) { 133 | fonts[fcnt] = { 134 | source_path: selectedFile.name, 135 | source_bin: Buffer.from(selectedFile.data), 136 | ranges: [ { range : [], symbols:'' } ] }; 137 | fonts[fcnt].ranges[0].symbols = syms; 138 | let r_sub = r_str.split(','); 139 | if (r_str.length) { 140 | // Parse the ranges. A range is array with 3 elements: 141 | //[range start, range end, range remap start] 142 | // Multiple ranges just means 143 | //an other 3 element in the array 144 | for (let i = 0; i < r_sub.length; i++) { 145 | let r = r_sub[i].split('-'); 146 | fonts[fcnt].ranges[0].range[i * 3 + 0] = parseInt(r[0]); 147 | if (r[1]) { 148 | fonts[fcnt].ranges[0].range[i * 3 + 1] = parseInt(r[1]); 149 | } else { 150 | fonts[fcnt].ranges[0].range[i * 3 + 1] = parseInt(r[0]); 151 | } 152 | fonts[fcnt].ranges[0].range[i * 3 + 2] = parseInt(r[0]); 153 | } 154 | } 155 | fcnt++; 156 | } 157 | }); 158 | 159 | const AppError = require('../lib/app_error'); 160 | 161 | const args = { 162 | font: fonts, 163 | size: parseInt(_size, 10), 164 | bpp: parseInt(_bpp, 10), 165 | no_compress : !(document.getElementById('compression').checked), 166 | lcd: document.getElementById('subpixel2').checked, 167 | lcd_v: document.getElementById('subpixel3').checked, 168 | use_color_info: document.getElementById('use_color_info').checked, 169 | format: 'lvgl', 170 | output: _name, 171 | lv_fallback: _fallback 172 | }; 173 | 174 | args.opts_string = generate_opts_string(args); 175 | 176 | convert(args).then(result => { 177 | const blob = new Blob([ result[_name] ], { type: 'text/plain;charset=utf-8' }); 178 | 179 | FileSaver.saveAs(blob, _name + '.c'); 180 | }).catch(err => { 181 | /*eslint-disable no-alert*/ 182 | // Try to beautify normal errors 183 | if (err instanceof AppError) { 184 | alert(err.message.trim()); 185 | return; 186 | } 187 | 188 | alert(err); 189 | 190 | /* eslint-disable no-console */ 191 | console.error(err); 192 | }); 193 | }, false); 194 | --------------------------------------------------------------------------------