├── .eslintrc.yml ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── math.js ├── sfnt.js ├── str.js ├── svg.js ├── ttf.js ├── ttf │ ├── tables │ │ ├── cmap.js │ │ ├── glyf.js │ │ ├── gsub.js │ │ ├── head.js │ │ ├── hhea.js │ │ ├── hmtx.js │ │ ├── loca.js │ │ ├── maxp.js │ │ ├── name.js │ │ ├── os2.js │ │ └── post.js │ └── utils.js └── ucs2.js ├── package.json ├── svg2ttf.js ├── test └── test.js └── ttfinfo.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | 5 | rules: 6 | accessor-pairs: 2 7 | array-bracket-spacing: [ 2, "always", { "singleValue": true, "objectsInArrays": true, "arraysInArrays": true } ] 8 | block-scoped-var: 2 9 | block-spacing: 2 10 | brace-style: [ 2, '1tbs', { "allowSingleLine": true } ] 11 | comma-dangle: 2 12 | comma-spacing: 2 13 | comma-style: 2 14 | computed-property-spacing: [ 2, never ] 15 | # Postponed 16 | #consistent-return: 2 17 | consistent-this: [ 2, self ] 18 | curly: [ 2, 'multi-line' ] 19 | # Postponed 20 | # dot-notation: [ 2, { allowKeywords: true } ] 21 | dot-location: [ 2, 'property' ] 22 | eol-last: 2 23 | eqeqeq: 2 24 | func-style: [ 2, declaration ] 25 | # Postponed 26 | #global-require: 2 27 | guard-for-in: 2 28 | handle-callback-err: 2 29 | indent: [ 2, 2, { VariableDeclarator: { var: 2, let: 2, const: 3 }, SwitchCase: 1 } ] 30 | keyword-spacing: 2 31 | linebreak-style: 2 32 | max-depth: [ 1, 5 ] 33 | max-nested-callbacks: [ 1, 5 ] 34 | # string can exceed 80 chars, but should not overflow github website :) 35 | #max-len: [ 2, 120, 1000 ] 36 | new-cap: 2 37 | new-parens: 2 38 | # Postponed 39 | newline-after-var: 2 40 | no-alert: 2 41 | no-array-constructor: 2 42 | #no-bitwise: 2 43 | no-caller: 2 44 | no-catch-shadow: 2 45 | no-cond-assign: 2 46 | no-console: 1 47 | no-constant-condition: 2 48 | no-control-regex: 2 49 | no-debugger: 1 50 | no-delete-var: 2 51 | no-div-regex: 2 52 | no-dupe-args: 2 53 | no-dupe-keys: 2 54 | no-duplicate-case: 2 55 | no-else-return: 2 56 | no-empty-character-class: 2 57 | no-empty-pattern: 2 58 | no-eq-null: 2 59 | no-eval: 2 60 | no-ex-assign: 2 61 | no-extend-native: 2 62 | no-extra-bind: 2 63 | no-extra-boolean-cast: 2 64 | no-extra-semi: 2 65 | no-fallthrough: 2 66 | no-floating-decimal: 2 67 | no-func-assign: 2 68 | # Postponed 69 | #no-implicit-coercion: [2, { "boolean": true, "number": true, "string": true } ] 70 | no-implied-eval: 2 71 | no-inner-declarations: 2 72 | no-invalid-regexp: 2 73 | no-irregular-whitespace: 2 74 | no-iterator: 2 75 | no-label-var: 2 76 | no-labels: 2 77 | no-lone-blocks: 1 78 | no-lonely-if: 2 79 | no-loop-func: 2 80 | no-mixed-requires: [ 1, { "grouping": true } ] 81 | no-mixed-spaces-and-tabs: 2 82 | # Postponed 83 | #no-native-reassign: 2 84 | no-negated-in-lhs: 2 85 | # Postponed 86 | #no-nested-ternary: 2 87 | no-new: 2 88 | no-new-func: 2 89 | no-new-object: 2 90 | no-new-require: 2 91 | no-new-wrappers: 2 92 | no-obj-calls: 2 93 | no-octal: 2 94 | no-octal-escape: 2 95 | no-path-concat: 2 96 | no-proto: 2 97 | no-redeclare: 2 98 | # Postponed 99 | #no-regex-spaces: 2 100 | no-return-assign: 2 101 | no-self-compare: 2 102 | no-sequences: 2 103 | # Postponed 104 | #no-shadow: 2 105 | no-shadow-restricted-names: 2 106 | no-sparse-arrays: 2 107 | # Postponed 108 | #no-sync: 2 109 | no-trailing-spaces: 2 110 | no-undef: 2 111 | no-undef-init: 2 112 | no-undefined: 2 113 | no-unexpected-multiline: 2 114 | no-unreachable: 2 115 | no-unused-expressions: 2 116 | no-unused-vars: 2 117 | no-use-before-define: 2 118 | no-void: 2 119 | no-with: 2 120 | object-curly-spacing: [ 2, always, { "objectsInObjects": true, "arraysInObjects": true } ] 121 | #operator-assignment: 1 122 | # Postponed 123 | #operator-linebreak: [ 2, after ] 124 | semi: 2 125 | semi-spacing: 2 126 | space-before-function-paren: [ 2, { "anonymous": "always", "named": "never" } ] 127 | space-in-parens: [ 2, never ] 128 | space-infix-ops: 2 129 | space-unary-ops: 2 130 | # Postponed 131 | #spaced-comment: [ 1, always, { exceptions: [ '/', '=' ] } ] 132 | strict: [ 2, global ] 133 | quotes: [ 2, single, avoid-escape ] 134 | quote-props: [ 1, 'as-needed', { "keywords": true } ] 135 | radix: 2 136 | use-isnan: 2 137 | valid-typeof: 2 138 | yoda: [ 2, never, { "exceptRange": true } ] 139 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: puzrin 2 | patreon: puzrin 3 | tidelift: "npm/svg2ttf" 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * 3' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v2 16 | 17 | - run: npm install 18 | - run: npm test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 6.0.3 / 2021-08-21 2 | ------------------ 3 | 4 | - xmldom => @xmldom/xmldom 5 | 6 | 7 | 6.0.2 / 2021-07-01 8 | ------------------ 9 | 10 | - Glyphs that don't fit in int32 now throw an error instead of generating invalid font, #113. 11 | 12 | 13 | 6.0.1 / 2021-06-17 14 | ------------------ 15 | 16 | - `OS/2` table version reduced (5 => 4), #110. 17 | 18 | 19 | 6.0.0 / 2021-05-27 20 | ------------------ 21 | 22 | - Big thanks to @yisibl for provided notes & samples. Those caused all fixes below. 23 | - `OS/2` table version bumped from v1 to v5 (#106). 24 | - `usWinAscent` should include `lineGap`. 25 | - Fixed `cubic2quad` dependency for edge cases. 26 | 27 | 28 | 5.2.0 / 2021-04-07 29 | ------------------ 30 | 31 | - Bump dependencies. 32 | - Migrate CI to github. 33 | 34 | 35 | 5.1.0 / 2020-10-29 36 | ------------------ 37 | 38 | - Added support of `d` attr in nested `` tag (for UNSCII font source). 39 | 40 | 41 | 42 | 5.0.0 / 2020-05-20 43 | ------------------ 44 | 45 | - Breaking: `USE_TYPO_METRICS` bit of `OS/2.fsSelection` is now set. Should fix 46 | Windows line spacing issues, #95. 47 | - Use font weight value when defined. 48 | 49 | 50 | 4.3.0 / 2019-05-24 51 | ------------------ 52 | 53 | - Add underline thickness/position support, #80. 54 | 55 | 56 | 4.2.1 / 2019-05-23 57 | ------------------ 58 | 59 | - Fix "new Bufer" deprecation warnings, #78. 60 | 61 | 62 | 4.2.0 / 2018-12-10 63 | ------------------ 64 | 65 | - Added `description` and `url` options, #74 66 | 67 | 68 | 4.1.0 / 2017-06-24 69 | ------------------ 70 | 71 | - Added font subfamily name support, #57. 72 | 73 | 74 | 4.0.3 / 2017-05-27 75 | ------------------ 76 | 77 | - Script tags should be arranged alpabetically, #55. 78 | 79 | 80 | 4.0.2 / 2016-08-04 81 | ------------------ 82 | 83 | - Added option to customize version string. 84 | 85 | 86 | 4.0.1 / 2016-06-03 87 | ------------------ 88 | 89 | - Fix IE ligatures by explicitly adding the latin script to the script list, #47. 90 | 91 | 92 | 4.0.0 / 2016-03-08 93 | ------------------ 94 | 95 | - Deps update (lodash -> 4.+). 96 | - Set HHEA lineGap to 0, #37 (second attempt :). 97 | - Cleanup, testing. 98 | 99 | 100 | 3.0.0 / 2016-02-12 101 | ------------------ 102 | 103 | - Changed defaults to workaround IE bugs. 104 | - Set HHEA lineGap to 0, #37. 105 | - Set OS/2 fsType to 0, #45. 106 | 107 | 108 | 2.1.1 / 2015-12-22 109 | ------------------ 110 | 111 | - Maintenance release: deps bump (svgpath with bugfixes). 112 | 113 | 114 | 2.1.0 / 2015-10-28 115 | ------------------ 116 | 117 | - Fixed smoothness at the ends of interpolated cubic beziers. 118 | 119 | 120 | 2.0.2 / 2015-08-23 121 | ------------------ 122 | 123 | - Fixed parse empty SVG glyphs without `d` attribute. 124 | 125 | 126 | 2.0.1 / 2015-07-17 127 | ------------------ 128 | 129 | - Fix: TTF creation timestamp should not depende on timezone, thanks to @nfroidure. 130 | 131 | 132 | 2.0.0 / 2015-04-25 133 | ------------------ 134 | 135 | - Added ligatures support, big thanks to @sabberworm. 136 | - Added arcs support in SVG paths. 137 | - Added `--ts` option to override default timestamp. 138 | - Fixed horisontal offset (`lsb`) when glyph exceed width. 139 | - Allow zero-width glyphs. 140 | - Better error message on attempt to convert SVG image instead of SVG font. 141 | 142 | 143 | 1.2.0 / 2014-10-05 144 | ------------------ 145 | 146 | - Fixed usWinAscent/usWinDescent - should not go below ascent/descent. 147 | - Upgraded ByteBuffer internal lib. 148 | - Code cleanup. 149 | 150 | 151 | 1.1.2 / 2013-12-02 152 | ------------------ 153 | 154 | - Fixed crash on SVG with empty (#8) 155 | - Fixed descent when input font has descent = 0 (@nfroidure) 156 | 157 | 158 | 1.1.1 / 2013-09-26 159 | ------------------ 160 | 161 | - SVG parser moved to external package 162 | - Speed opts 163 | - Code refactoring/cleanup 164 | 165 | 166 | 1.1.0 / 2013-09-25 167 | ------------------ 168 | 169 | - Rewritten svg parser to improve speed 170 | - API changed, now returns buffer as array/Uint8Array 171 | 172 | 173 | 1.0.7 / 2013-09-22 174 | ------------------ 175 | 176 | - Improved speed x2.5 times 177 | 178 | 179 | 1.0.6 / 2013-09-12 180 | ------------------ 181 | 182 | - Improved handling glyphs without codes or names 183 | - Fixed crash on glyphs with `v`/`h` commands 184 | - Logic cleanup 185 | 186 | 187 | 1.0.5 / 2013-08-27 188 | ------------------ 189 | 190 | - Added CLI option `-c` to set copyright string 191 | - Fixed crash when some metrics missed in source SVG 192 | - Minor code cleanup 193 | 194 | 195 | 1.0.4 / 2013-08-09 196 | ------------------ 197 | 198 | - Fixed importing into OSX Font Book 199 | 200 | 201 | 1.0.3 / 2013-08-02 202 | ------------------ 203 | 204 | - Fixed maxp table max points count (solved chrome problems under windozze) 205 | 206 | 207 | 1.0.2 / 2013-07-24 208 | ------------------ 209 | 210 | - Fixed htmx table size 211 | - Fixed loca table size for long format 212 | - Fixed glyph bounding boxes writing 213 | 214 | 215 | 1.0.1 / 2013-07-24 216 | ------------------ 217 | 218 | - Added options support 219 | - Added `ttfinfo` utility 220 | - Multiple fixes 221 | 222 | 223 | 1.0.0 / 2013-07-19 224 | ------------------ 225 | 226 | - First release 227 | 228 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (C) 2013-2015 by Vitaly Puzrin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | svg2ttf 2 | ======= 3 | 4 | [![CI](https://github.com/fontello/svg2ttf/actions/workflows/ci.yml/badge.svg)](https://github.com/fontello/svg2ttf/actions/workflows/ci.yml) 5 | [![NPM version](https://img.shields.io/npm/v/svg2ttf.svg?style=flat)](https://www.npmjs.org/package/svg2ttf) 6 | 7 | > Converts SVG fonts to TTF format. It was initially written for 8 | [Fontello](http://fontello.com), but you can find it useful for your projects. 9 | 10 | __For developpers:__ 11 | 12 | Internal API is similar to FontForge's one. Since primary goal 13 | is generating iconic fonts, sources can lack some specific TTF/OTF features, 14 | like kerning and so on. Anyway, current code is a good base for development, 15 | because it will save you tons of hours to implement correct writing & optimizing 16 | TTF tables. 17 | 18 | 19 | Using from CLI 20 | ---------------- 21 | 22 | Install: 23 | 24 | ``` bash 25 | npm install -g svg2ttf 26 | ``` 27 | 28 | Usage example: 29 | 30 | ``` bash 31 | svg2ttf fontello.svg fontello.ttf 32 | ``` 33 | 34 | 35 | API 36 | --- 37 | 38 | ### svg2ttf(svgFontString, options) -> buf 39 | 40 | - `svgFontString` - SVG font content 41 | - `options` 42 | - `copyright` - copyright string (optional) 43 | - `description` - description string (optional) 44 | - `ts` - Unix timestamp (in seconds) to override creation time (optional) 45 | - `url` - manufacturer url (optional) 46 | - `version` - font version string, can be `Version x.y` or `x.y`. 47 | - `buf` - internal [byte buffer](https://github.com/fontello/microbuffer) 48 | object, similar to DataView. It's `buffer` property is `Uin8Array` or `Array` 49 | with ttf content. 50 | 51 | Example: 52 | 53 | ``` javascript 54 | var fs = require('fs'); 55 | var svg2ttf = require('svg2ttf'); 56 | 57 | var ttf = svg2ttf(fs.readFileSync('myfont.svg', 'utf8'), {}); 58 | fs.writeFileSync('myfont.ttf', new Buffer(ttf.buffer)); 59 | ``` 60 | 61 | 62 | ## svg2ttf for enterprise 63 | 64 | Available as part of the Tidelift Subscription. 65 | 66 | The maintainers of `svg2ttf` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-svg2ttf?utm_source=npm-svg2ttf&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright: Vitaly Puzrin 3 | * Author: Sergey Batishchev 4 | * 5 | * Written for fontello.com project. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | var _ = require('lodash'); 11 | var SvgPath = require('svgpath'); 12 | var ucs2 = require('./lib/ucs2'); 13 | var svg = require('./lib/svg'); 14 | var sfnt = require('./lib/sfnt'); 15 | 16 | 17 | var VERSION_RE = /^(Version )?(\d+[.]\d+)$/i; 18 | 19 | 20 | function svg2ttf(svgString, options) { 21 | var font = new sfnt.Font(); 22 | var svgFont = svg.load(svgString); 23 | 24 | options = options || {}; 25 | 26 | font.id = options.id || svgFont.id; 27 | font.familyName = options.familyname || svgFont.familyName || svgFont.id; 28 | font.copyright = options.copyright || svgFont.metadata; 29 | font.description = options.description || 'Generated by svg2ttf from Fontello project.'; 30 | font.url = options.url || 'http://fontello.com'; 31 | font.sfntNames.push({ id: 2, value: options.subfamilyname || svgFont.subfamilyName || 'Regular' }); // subfamily name 32 | font.sfntNames.push({ id: 4, value: options.fullname || svgFont.id }); // full name 33 | 34 | var versionString = options.version || 'Version 1.0'; 35 | 36 | if (typeof versionString !== 'string') { 37 | throw new Error('svg2ttf: version option should be a string'); 38 | } 39 | if (!VERSION_RE.test(versionString)) { 40 | throw new Error('svg2ttf: invalid option, version - "' + options.version + '"'); 41 | } 42 | 43 | versionString = 'Version ' + versionString.match(VERSION_RE)[2]; 44 | font.sfntNames.push({ id: 5, value: versionString }); // version ID for TTF name table 45 | font.sfntNames.push({ id: 6, value: (options.fullname || svgFont.id).replace(/[\s\(\)\[\]<>%\/]/g, '').substr(0, 62) }); // Postscript name for the font, required for OSX Font Book 46 | 47 | if (typeof options.ts !== 'undefined') { 48 | font.createdDate = font.modifiedDate = new Date(parseInt(options.ts, 10) * 1000); 49 | } 50 | 51 | // Try to fill font metrics or guess defaults 52 | // 53 | font.unitsPerEm = svgFont.unitsPerEm || 1000; 54 | font.horizOriginX = svgFont.horizOriginX || 0; 55 | font.horizOriginY = svgFont.horizOriginY || 0; 56 | font.vertOriginX = svgFont.vertOriginX || 0; 57 | font.vertOriginY = svgFont.vertOriginY || 0; 58 | font.width = svgFont.width || svgFont.unitsPerEm; 59 | font.height = svgFont.height || svgFont.unitsPerEm; 60 | font.descent = !isNaN(svgFont.descent) ? svgFont.descent : -font.vertOriginY; 61 | font.ascent = svgFont.ascent || (font.unitsPerEm - font.vertOriginY); 62 | // Values for font substitution. We're mostly working with icon fonts, so they aren't expected to be substituted. 63 | // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#sxheight 64 | font.capHeight = svgFont.capHeight || 0; // 0 is a valid value if "H" glyph doesn't exist 65 | font.xHeight = svgFont.xHeight || 0; // 0 is a valid value if "x" glyph doesn't exist 66 | 67 | if (typeof svgFont.weightClass !== 'undefined') { 68 | var wght = parseInt(svgFont.weightClass, 10); 69 | 70 | if (!isNaN(wght)) font.weightClass = wght; 71 | else { 72 | // Unknown names are silently ignored 73 | if (svgFont.weightClass === 'normal') font.weightClass = 400; 74 | if (svgFont.weightClass === 'bold') font.weightClass = 700; 75 | } 76 | } 77 | 78 | 79 | if (typeof svgFont.underlinePosition !== 'undefined') { 80 | font.underlinePosition = svgFont.underlinePosition; 81 | } 82 | if (typeof svgFont.underlineThickness !== 'undefined') { 83 | font.underlineThickness = svgFont.underlineThickness; 84 | } 85 | 86 | var glyphs = font.glyphs; 87 | var codePoints = font.codePoints; 88 | var ligatures = font.ligatures; 89 | 90 | function addCodePoint(codePoint, glyph) { 91 | if (codePoints[codePoint]) { 92 | // Ignore code points already defined 93 | return false; 94 | } 95 | codePoints[codePoint] = glyph; 96 | return true; 97 | } 98 | 99 | // add SVG glyphs to SFNT font 100 | _.forEach(svgFont.glyphs, function (svgGlyph) { 101 | var glyph = new sfnt.Glyph(); 102 | 103 | glyph.name = svgGlyph.name; 104 | glyph.codes = svgGlyph.ligatureCodes || svgGlyph.unicode; // needed for nice validator error output 105 | glyph.d = svgGlyph.d; 106 | glyph.height = !isNaN(svgGlyph.height) ? svgGlyph.height : font.height; 107 | glyph.width = !isNaN(svgGlyph.width) ? svgGlyph.width : font.width; 108 | glyphs.push(glyph); 109 | 110 | svgGlyph.sfntGlyph = glyph; 111 | 112 | _.forEach(svgGlyph.unicode, function (codePoint) { 113 | addCodePoint(codePoint, glyph); 114 | }); 115 | }); 116 | 117 | var missingGlyph; 118 | 119 | // add missing glyph to SFNT font 120 | // also, check missing glyph existance and single instance 121 | if (svgFont.missingGlyph) { 122 | missingGlyph = new sfnt.Glyph(); 123 | missingGlyph.d = svgFont.missingGlyph.d; 124 | missingGlyph.height = !isNaN(svgFont.missingGlyph.height) ? svgFont.missingGlyph.height : font.height; 125 | missingGlyph.width = !isNaN(svgFont.missingGlyph.width) ? svgFont.missingGlyph.width : font.width; 126 | } else { 127 | missingGlyph = _.find(glyphs, function (glyph) { 128 | return glyph.name === '.notdef'; 129 | }); 130 | } 131 | if (!missingGlyph) { // no missing glyph and .notdef glyph, we need to create missing glyph 132 | missingGlyph = new sfnt.Glyph(); 133 | } 134 | 135 | // Create glyphs for all characters used in ligatures 136 | _.forEach(svgFont.ligatures, function (svgLigature) { 137 | var ligature = { 138 | ligature: svgLigature.ligature, 139 | unicode: svgLigature.unicode, 140 | glyph: svgLigature.glyph.sfntGlyph 141 | }; 142 | 143 | _.forEach(ligature.unicode, function (charPoint) { 144 | // We need to have a distinct glyph for each code point so we can reference it in GSUB 145 | var glyph = new sfnt.Glyph(); 146 | var added = addCodePoint(charPoint, glyph); 147 | 148 | if (added) { 149 | glyph.name = ucs2.encode([ charPoint ]); 150 | glyphs.push(glyph); 151 | } 152 | }); 153 | ligatures.push(ligature); 154 | }); 155 | 156 | // Missing Glyph needs to have index 0 157 | if (glyphs.indexOf(missingGlyph) !== -1) { 158 | glyphs.splice(glyphs.indexOf(missingGlyph), 1); 159 | } 160 | glyphs.unshift(missingGlyph); 161 | 162 | var nextID = 0; 163 | 164 | //add IDs 165 | _.forEach(glyphs, function (glyph) { 166 | glyph.id = nextID; 167 | nextID++; 168 | }); 169 | 170 | _.forEach(glyphs, function (glyph) { 171 | 172 | // Calculate accuracy for cubicToQuad transformation 173 | // For glyphs with height and width smaller than 500 use relative 0.06% accuracy, 174 | // for larger glyphs use fixed accuracy 0.3. 175 | var glyphSize = Math.max(glyph.width, glyph.height); 176 | var accuracy = (glyphSize > 500) ? 0.3 : glyphSize * 0.0006; 177 | 178 | //SVG transformations 179 | var svgPath = new SvgPath(glyph.d) 180 | .abs() 181 | .unshort() 182 | .unarc() 183 | .iterate(function (segment, index, x, y) { 184 | return svg.cubicToQuad(segment, index, x, y, accuracy); 185 | }); 186 | var sfntContours = svg.toSfntCoutours(svgPath); 187 | 188 | // Add contours to SFNT font 189 | glyph.contours = _.map(sfntContours, function (sfntContour) { 190 | var contour = new sfnt.Contour(); 191 | 192 | contour.points = _.map(sfntContour, function (sfntPoint) { 193 | var point = new sfnt.Point(); 194 | 195 | point.x = sfntPoint.x; 196 | point.y = sfntPoint.y; 197 | point.onCurve = sfntPoint.onCurve; 198 | return point; 199 | }); 200 | 201 | return contour; 202 | }); 203 | }); 204 | 205 | var ttf = sfnt.toTTF(font); 206 | 207 | return ttf; 208 | } 209 | 210 | module.exports = svg2ttf; 211 | -------------------------------------------------------------------------------- /lib/math.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Point(x, y) { 4 | this.x = x; 5 | this.y = y; 6 | } 7 | 8 | Point.prototype.add = function (point) { 9 | return new Point(this.x + point.x, this.y + point.y); 10 | }; 11 | 12 | Point.prototype.sub = function (point) { 13 | return new Point(this.x - point.x, this.y - point.y); 14 | }; 15 | 16 | Point.prototype.mul = function (value) { 17 | return new Point(this.x * value, this.y * value); 18 | }; 19 | 20 | Point.prototype.div = function (value) { 21 | return new Point(this.x / value, this.y / value); 22 | }; 23 | 24 | Point.prototype.dist = function () { 25 | return Math.sqrt(this.x * this.x + this.y * this.y); 26 | }; 27 | 28 | Point.prototype.sqr = function () { 29 | return this.x * this.x + this.y * this.y; 30 | }; 31 | 32 | /* 33 | * Check if 3 points are in line, and second in the midle. 34 | * Used to replace quad curves with lines or join lines 35 | * 36 | */ 37 | function isInLine(p1, m, p2, accuracy) { 38 | var a = p1.sub(m).sqr(); 39 | var b = p2.sub(m).sqr(); 40 | var c = p1.sub(p2).sqr(); 41 | 42 | // control point not between anchors 43 | if ((a > (b + c)) || (b > (a + c))) { 44 | return false; 45 | } 46 | 47 | // count distance via scalar multiplication 48 | var distance = Math.sqrt(Math.pow((p1.x - m.x) * (p2.y - m.y) - (p2.x - m.x) * (p1.y - m.y), 2) / c); 49 | 50 | return distance < accuracy ? true : false; 51 | } 52 | 53 | module.exports.Point = Point; 54 | module.exports.isInLine = isInLine; 55 | -------------------------------------------------------------------------------- /lib/sfnt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | function Font() { 6 | this.ascent = 850; 7 | this.copyright = ''; 8 | this.createdDate = new Date(); 9 | this.glyphs = []; 10 | this.ligatures = []; 11 | // Maping of code points to glyphs. 12 | // Keys are actually numeric, thus should be `parseInt`ed. 13 | this.codePoints = {}; 14 | this.isFixedPitch = 0; 15 | this.italicAngle = 0; 16 | this.familyClass = 0; // No Classification 17 | this.familyName = ''; 18 | 19 | // 0x40 - REGULAR - Characters are in the standard weight/style for the font 20 | // 0x80 - USE_TYPO_METRICS - use OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap as the default line spacing 21 | // https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fsselection 22 | // https://github.com/fontello/svg2ttf/issues/95 23 | this.fsSelection = 0x40 | 0x80; 24 | 25 | // Non zero value can cause issues in IE, https://github.com/fontello/svg2ttf/issues/45 26 | this.fsType = 0; 27 | this.lowestRecPPEM = 8; 28 | this.macStyle = 0; 29 | this.modifiedDate = new Date(); 30 | this.panose = { 31 | familyType: 2, // Latin Text 32 | serifStyle: 0, // any 33 | weight: 5, // book 34 | proportion: 3, //modern 35 | contrast: 0, //any 36 | strokeVariation: 0, //any, 37 | armStyle: 0, //any, 38 | letterform: 0, //any, 39 | midline: 0, //any, 40 | xHeight: 0 //any, 41 | }; 42 | this.revision = 1; 43 | this.sfntNames = []; 44 | this.underlineThickness = 0; 45 | this.unitsPerEm = 1000; 46 | this.weightClass = 400; // normal 47 | this.width = 1000; 48 | this.widthClass = 5; // Medium (normal) 49 | this.ySubscriptXOffset = 0; 50 | this.ySuperscriptXOffset = 0; 51 | this.int_descent = -150; 52 | this.xHeight = 0; 53 | this.capHeight = 0; 54 | 55 | //getters and setters 56 | 57 | Object.defineProperty(this, 'descent', { 58 | get: function () { 59 | return this.int_descent; 60 | }, 61 | set: function (value) { 62 | this.int_descent = parseInt(Math.round(-Math.abs(value)), 10); 63 | } 64 | }); 65 | 66 | this.__defineGetter__('avgCharWidth', function () { 67 | if (this.glyphs.length === 0) { 68 | return 0; 69 | } 70 | var widths = _.map(this.glyphs, 'width'); 71 | 72 | return parseInt(widths.reduce(function (prev, cur) { 73 | return prev + cur; 74 | }) / widths.length, 10); 75 | }); 76 | 77 | Object.defineProperty(this, 'ySubscriptXSize', { 78 | get: function () { 79 | return parseInt(!_.isUndefined(this.int_ySubscriptXSize) ? this.int_ySubscriptXSize : (this.width * 0.6347), 10); 80 | }, 81 | set: function (value) { 82 | this.int_ySubscriptXSize = value; 83 | } 84 | }); 85 | 86 | Object.defineProperty(this, 'ySubscriptYSize', { 87 | get: function () { 88 | return parseInt(!_.isUndefined(this.int_ySubscriptYSize) ? this.int_ySubscriptYSize : ((this.ascent - this.descent) * 0.7), 10); 89 | }, 90 | set: function (value) { 91 | this.int_ySubscriptYSize = value; 92 | } 93 | }); 94 | 95 | Object.defineProperty(this, 'ySubscriptYOffset', { 96 | get: function () { 97 | return parseInt(!_.isUndefined(this.int_ySubscriptYOffset) ? this.int_ySubscriptYOffset : ((this.ascent - this.descent) * 0.14), 10); 98 | }, 99 | set: function (value) { 100 | this.int_ySubscriptYOffset = value; 101 | } 102 | }); 103 | 104 | Object.defineProperty(this, 'ySuperscriptXSize', { 105 | get: function () { 106 | return parseInt(!_.isUndefined(this.int_ySuperscriptXSize) ? this.int_ySuperscriptXSize : (this.width * 0.6347), 10); 107 | }, 108 | set: function (value) { 109 | this.int_ySuperscriptXSize = value; 110 | } 111 | }); 112 | 113 | Object.defineProperty(this, 'ySuperscriptYSize', { 114 | get: function () { 115 | return parseInt(!_.isUndefined(this.int_ySuperscriptYSize) ? this.int_ySuperscriptYSize : ((this.ascent - this.descent) * 0.7), 10); 116 | }, 117 | set: function (value) { 118 | this.int_ySuperscriptYSize = value; 119 | } 120 | }); 121 | 122 | Object.defineProperty(this, 'ySuperscriptYOffset', { 123 | get: function () { 124 | return parseInt(!_.isUndefined(this.int_ySuperscriptYOffset) ? this.int_ySuperscriptYOffset : ((this.ascent - this.descent) * 0.48), 10); 125 | }, 126 | set: function (value) { 127 | this.int_ySuperscriptYOffset = value; 128 | } 129 | }); 130 | 131 | Object.defineProperty(this, 'yStrikeoutSize', { 132 | get: function () { 133 | return parseInt(!_.isUndefined(this.int_yStrikeoutSize) ? this.int_yStrikeoutSize : ((this.ascent - this.descent) * 0.049), 10); 134 | }, 135 | set: function (value) { 136 | this.int_yStrikeoutSize = value; 137 | } 138 | }); 139 | 140 | Object.defineProperty(this, 'yStrikeoutPosition', { 141 | get: function () { 142 | return parseInt(!_.isUndefined(this.int_yStrikeoutPosition) ? this.int_yStrikeoutPosition : ((this.ascent - this.descent) * 0.258), 10); 143 | }, 144 | set: function (value) { 145 | this.int_yStrikeoutPosition = value; 146 | } 147 | }); 148 | 149 | Object.defineProperty(this, 'minLsb', { 150 | get: function () { 151 | return parseInt(_.min(_.map(this.glyphs, 'xMin')), 10); 152 | } 153 | }); 154 | 155 | Object.defineProperty(this, 'minRsb', { 156 | get: function () { 157 | if (!this.glyphs.length) return parseInt(this.width, 10); 158 | 159 | return parseInt(_.reduce(this.glyphs, function (minRsb, glyph) { 160 | return Math.min(minRsb, glyph.width - glyph.xMax); 161 | }, 0), 10); 162 | } 163 | }); 164 | 165 | Object.defineProperty(this, 'xMin', { 166 | get: function () { 167 | if (!this.glyphs.length) return this.width; 168 | 169 | return _.reduce(this.glyphs, function (xMin, glyph) { 170 | return Math.min(xMin, glyph.xMin); 171 | }, 0); 172 | } 173 | }); 174 | 175 | Object.defineProperty(this, 'yMin', { 176 | get: function () { 177 | if (!this.glyphs.length) return this.width; 178 | 179 | return _.reduce(this.glyphs, function (yMin, glyph) { 180 | return Math.min(yMin, glyph.yMin); 181 | }, 0); 182 | } 183 | }); 184 | 185 | Object.defineProperty(this, 'xMax', { 186 | get: function () { 187 | if (!this.glyphs.length) return this.width; 188 | 189 | return _.reduce(this.glyphs, function (xMax, glyph) { 190 | return Math.max(xMax, glyph.xMax); 191 | }, 0); 192 | } 193 | }); 194 | 195 | Object.defineProperty(this, 'yMax', { 196 | get: function () { 197 | if (!this.glyphs.length) return this.width; 198 | 199 | return _.reduce(this.glyphs, function (yMax, glyph) { 200 | return Math.max(yMax, glyph.yMax); 201 | }, 0); 202 | } 203 | }); 204 | 205 | Object.defineProperty(this, 'avgWidth', { 206 | get: function () { 207 | var len = this.glyphs.length; 208 | 209 | if (len === 0) { 210 | return this.width; 211 | } 212 | 213 | var sumWidth = _.reduce(this.glyphs, function (sumWidth, glyph) { 214 | return sumWidth + glyph.width; 215 | }, 0); 216 | 217 | return Math.round(sumWidth / len); 218 | } 219 | }); 220 | 221 | Object.defineProperty(this, 'maxWidth', { 222 | get: function () { 223 | if (!this.glyphs.length) return this.width; 224 | 225 | return _.reduce(this.glyphs, function (maxWidth, glyph) { 226 | return Math.max(maxWidth, glyph.width); 227 | }, 0); 228 | } 229 | }); 230 | 231 | Object.defineProperty(this, 'maxExtent', { 232 | get: function () { 233 | if (!this.glyphs.length) return this.width; 234 | 235 | return _.reduce(this.glyphs, function (maxExtent, glyph) { 236 | return Math.max(maxExtent, glyph.xMax /*- glyph.xMin*/); 237 | }, 0); 238 | } 239 | }); 240 | 241 | // Property used for `sTypoLineGap` in OS/2 and not used for `lineGap` in HHEA, because 242 | // non zero lineGap causes bad offset in IE, https://github.com/fontello/svg2ttf/issues/37 243 | Object.defineProperty(this, 'lineGap', { 244 | get: function () { 245 | return parseInt(!_.isUndefined(this.int_lineGap) ? this.int_lineGap : ((this.ascent - this.descent) * 0.09), 10); 246 | }, 247 | set: function (value) { 248 | this.int_lineGap = value; 249 | } 250 | }); 251 | 252 | Object.defineProperty(this, 'underlinePosition', { 253 | get: function () { 254 | return parseInt(!_.isUndefined(this.int_underlinePosition) ? this.int_underlinePosition : ((this.ascent - this.descent) * 0.01), 10); 255 | }, 256 | set: function (value) { 257 | this.int_underlinePosition = value; 258 | } 259 | }); 260 | } 261 | 262 | 263 | function Glyph() { 264 | this.contours = []; 265 | this.d = ''; 266 | this.id = ''; 267 | this.codes = []; // needed for nice validator error output 268 | this.height = 0; 269 | this.name = ''; 270 | this.width = 0; 271 | } 272 | 273 | Object.defineProperty(Glyph.prototype, 'xMin', { 274 | get: function () { 275 | var xMin = 0; 276 | var hasPoints = false; 277 | 278 | _.forEach(this.contours, function (contour) { 279 | _.forEach(contour.points, function (point) { 280 | xMin = Math.min(xMin, Math.floor(point.x)); 281 | hasPoints = true; 282 | }); 283 | 284 | }); 285 | 286 | if (xMin < -32768) { 287 | throw new Error('xMin value for glyph ' + (this.name ? ('"' + this.name + '"') : JSON.stringify(this.codes)) + 288 | ' is out of bounds (actual ' + xMin + ', expected -32768..32767, d="' + this.d + '")'); 289 | } 290 | return hasPoints ? xMin : 0; 291 | } 292 | }); 293 | 294 | Object.defineProperty(Glyph.prototype, 'xMax', { 295 | get: function () { 296 | var xMax = 0; 297 | var hasPoints = false; 298 | 299 | _.forEach(this.contours, function (contour) { 300 | _.forEach(contour.points, function (point) { 301 | xMax = Math.max(xMax, -Math.floor(-point.x)); 302 | hasPoints = true; 303 | }); 304 | 305 | }); 306 | 307 | if (xMax > 32767) { 308 | throw new Error('xMax value for glyph ' + (this.name ? ('"' + this.name + '"') : JSON.stringify(this.codes)) + 309 | ' is out of bounds (actual ' + xMax + ', expected -32768..32767, d="' + this.d + '")'); 310 | } 311 | return hasPoints ? xMax : this.width; 312 | } 313 | }); 314 | 315 | Object.defineProperty(Glyph.prototype, 'yMin', { 316 | get: function () { 317 | var yMin = 0; 318 | var hasPoints = false; 319 | 320 | _.forEach(this.contours, function (contour) { 321 | _.forEach(contour.points, function (point) { 322 | yMin = Math.min(yMin, Math.floor(point.y)); 323 | hasPoints = true; 324 | }); 325 | 326 | }); 327 | 328 | if (yMin < -32768) { 329 | throw new Error('yMin value for glyph ' + (this.name ? ('"' + this.name + '"') : JSON.stringify(this.codes)) + 330 | ' is out of bounds (actual ' + yMin + ', expected -32768..32767, d="' + this.d + '")'); 331 | } 332 | return hasPoints ? yMin : 0; 333 | } 334 | }); 335 | 336 | Object.defineProperty(Glyph.prototype, 'yMax', { 337 | get: function () { 338 | var yMax = 0; 339 | var hasPoints = false; 340 | 341 | _.forEach(this.contours, function (contour) { 342 | _.forEach(contour.points, function (point) { 343 | yMax = Math.max(yMax, -Math.floor(-point.y)); 344 | hasPoints = true; 345 | }); 346 | 347 | }); 348 | 349 | if (yMax > 32767) { 350 | throw new Error('yMax value for glyph ' + (this.name ? ('"' + this.name + '"') : JSON.stringify(this.codes)) + 351 | ' is out of bounds (actual ' + yMax + ', expected -32768..32767, d="' + this.d + '")'); 352 | } 353 | return hasPoints ? yMax : 0; 354 | } 355 | }); 356 | 357 | function Contour() { 358 | this.points = []; 359 | } 360 | 361 | function Point() { 362 | this.onCurve = true; 363 | this.x = 0; 364 | this.y = 0; 365 | } 366 | 367 | function SfntName() { 368 | this.id = 0; 369 | this.value = ''; 370 | } 371 | 372 | module.exports.Font = Font; 373 | module.exports.Glyph = Glyph; 374 | module.exports.Contour = Contour; 375 | module.exports.Point = Point; 376 | module.exports.SfntName = SfntName; 377 | module.exports.toTTF = require('./ttf'); 378 | -------------------------------------------------------------------------------- /lib/str.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Str(str) { 4 | if (!(this instanceof Str)) { 5 | return new Str(str); 6 | } 7 | 8 | this.str = str; 9 | 10 | this.toUTF8Bytes = function () { 11 | 12 | var byteArray = []; 13 | 14 | for (var i = 0; i < str.length; i++) { 15 | if (str.charCodeAt(i) <= 0x7F) { 16 | byteArray.push(str.charCodeAt(i)); 17 | } else { 18 | var h = encodeURIComponent(str.charAt(i)).substr(1).split('%'); 19 | 20 | for (var j = 0; j < h.length; j++) { 21 | byteArray.push(parseInt(h[j], 16)); 22 | } 23 | } 24 | } 25 | return byteArray; 26 | }; 27 | 28 | this.toUCS2Bytes = function () { 29 | // Code is taken here: 30 | // http://stackoverflow.com/questions/6226189/how-to-convert-a-string-to-bytearray 31 | var byteArray = []; 32 | var ch; 33 | 34 | for (var i = 0; i < str.length; ++i) { 35 | ch = str.charCodeAt(i); // get char 36 | byteArray.push(ch >> 8); 37 | byteArray.push(ch & 0xFF); 38 | } 39 | return byteArray; 40 | }; 41 | } 42 | 43 | module.exports = Str; 44 | -------------------------------------------------------------------------------- /lib/svg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var cubic2quad = require('cubic2quad'); 5 | var svgpath = require('svgpath'); 6 | var DOMParser = require('@xmldom/xmldom').DOMParser; 7 | var ucs2 = require('./ucs2'); 8 | 9 | function getGlyph(glyphElem, fontInfo) { 10 | var glyph = {}; 11 | 12 | if (glyphElem.hasAttribute('d')) { 13 | glyph.d = glyphElem.getAttribute('d').trim(); 14 | } else { 15 | // try nested 16 | var pathElem = glyphElem.getElementsByTagName('path')[0]; 17 | 18 | if (pathElem.hasAttribute('d')) { 19 | // has reversed Y axis 20 | glyph.d = svgpath(pathElem.getAttribute('d')) 21 | .scale(1, -1) 22 | .translate(0, fontInfo.ascent) 23 | .toString(); 24 | } else { 25 | throw new Error("Can't find 'd' attribute of tag."); 26 | } 27 | } 28 | 29 | glyph.unicode = []; 30 | 31 | if (glyphElem.getAttribute('unicode')) { 32 | glyph.character = glyphElem.getAttribute('unicode'); 33 | var unicode = ucs2.decode(glyph.character); 34 | 35 | // If more than one code point is involved, the glyph is a ligature glyph 36 | if (unicode.length > 1) { 37 | glyph.ligature = glyph.character; 38 | glyph.ligatureCodes = unicode; 39 | } else { 40 | glyph.unicode.push(unicode[0]); 41 | } 42 | } 43 | 44 | glyph.name = glyphElem.getAttribute('glyph-name'); 45 | 46 | if (glyphElem.getAttribute('horiz-adv-x')) { 47 | glyph.width = parseInt(glyphElem.getAttribute('horiz-adv-x'), 10); 48 | } 49 | 50 | return glyph; 51 | } 52 | 53 | function deduplicateGlyps(glyphs, ligatures) { 54 | // Result (the list of unique glyphs) 55 | var result = []; 56 | 57 | _.forEach(glyphs, function (glyph) { 58 | // Search for glyphs with the same properties (width and d) 59 | var canonical = _.find(result, { width: glyph.width, d: glyph.d }); 60 | 61 | if (canonical) { 62 | // Add the code points to the unicode array. 63 | // The fields “name” and “character” are not that important so we leave them how we first enounter them and throw the rest away 64 | canonical.unicode = canonical.unicode.concat(glyph.unicode); 65 | glyph.canonical = canonical; 66 | } else { 67 | result.push(glyph); 68 | } 69 | }); 70 | 71 | // Update ligatures to point to the canonical version 72 | _.forEach(ligatures, function (ligature) { 73 | while (_.has(ligature.glyph, 'canonical')) { 74 | ligature.glyph = ligature.glyph.canonical; 75 | } 76 | }); 77 | 78 | return result; 79 | } 80 | 81 | function load(str) { 82 | var attrs; 83 | 84 | var doc = (new DOMParser()).parseFromString(str, 'application/xml'); 85 | 86 | var metadata, fontElem, fontFaceElem; 87 | 88 | metadata = doc.getElementsByTagName('metadata')[0]; 89 | fontElem = doc.getElementsByTagName('font')[0]; 90 | 91 | if (!fontElem) { 92 | throw new Error("Can't find tag. Make sure you SVG file is font, not image."); 93 | } 94 | 95 | fontFaceElem = fontElem.getElementsByTagName('font-face')[0]; 96 | 97 | var familyName = fontFaceElem.getAttribute('font-family') || 'fontello'; 98 | var subfamilyName = fontFaceElem.getAttribute('font-style') || 'Regular'; 99 | var id = fontElem.getAttribute('id') || (familyName + '-' + subfamilyName).replace(/[\s\(\)\[\]<>%\/]/g, '').substr(0, 62); 100 | 101 | var font = { 102 | id: id, 103 | familyName: familyName, 104 | subfamilyName: subfamilyName, 105 | stretch: fontFaceElem.getAttribute('font-stretch') || 'normal' 106 | }; 107 | 108 | // Doesn't work with complex content like Copyright:>Fontello 109 | if (metadata && metadata.textContent) { 110 | font.metadata = metadata.textContent; 111 | } 112 | 113 | // Get numeric attributes 114 | attrs = { 115 | width: 'horiz-adv-x', 116 | //height: 'vert-adv-y', 117 | horizOriginX: 'horiz-origin-x', 118 | horizOriginY: 'horiz-origin-y', 119 | vertOriginX: 'vert-origin-x', 120 | vertOriginY: 'vert-origin-y' 121 | }; 122 | _.forEach(attrs, function (val, key) { 123 | if (fontElem.hasAttribute(val)) { font[key] = parseInt(fontElem.getAttribute(val), 10); } 124 | }); 125 | 126 | // Get numeric attributes 127 | attrs = { 128 | ascent: 'ascent', 129 | descent: 'descent', 130 | unitsPerEm: 'units-per-em', 131 | capHeight: 'cap-height', 132 | xHeight: 'x-height', 133 | underlineThickness: 'underline-thickness', 134 | underlinePosition: 'underline-position' 135 | }; 136 | _.forEach(attrs, function (val, key) { 137 | if (fontFaceElem.hasAttribute(val)) { font[key] = parseInt(fontFaceElem.getAttribute(val), 10); } 138 | }); 139 | 140 | if (fontFaceElem.hasAttribute('font-weight')) { 141 | font.weightClass = fontFaceElem.getAttribute('font-weight'); 142 | } 143 | 144 | var missingGlyphElem = fontElem.getElementsByTagName('missing-glyph')[0]; 145 | 146 | if (missingGlyphElem) { 147 | 148 | font.missingGlyph = {}; 149 | font.missingGlyph.d = missingGlyphElem.getAttribute('d') || ''; 150 | 151 | if (missingGlyphElem.getAttribute('horiz-adv-x')) { 152 | font.missingGlyph.width = parseInt(missingGlyphElem.getAttribute('horiz-adv-x'), 10); 153 | } 154 | } 155 | 156 | var glyphs = []; 157 | var ligatures = []; 158 | 159 | _.forEach(fontElem.getElementsByTagName('glyph'), function (glyphElem) { 160 | var glyph = getGlyph(glyphElem, font); 161 | 162 | if (_.has(glyph, 'ligature')) { 163 | ligatures.push({ 164 | ligature: glyph.ligature, 165 | unicode: glyph.ligatureCodes, 166 | glyph: glyph 167 | }); 168 | } 169 | 170 | glyphs.push(glyph); 171 | }); 172 | 173 | glyphs = deduplicateGlyps(glyphs, ligatures); 174 | 175 | font.glyphs = glyphs; 176 | font.ligatures = ligatures; 177 | 178 | return font; 179 | } 180 | 181 | 182 | function cubicToQuad(segment, index, x, y, accuracy) { 183 | if (segment[0] === 'C') { 184 | var quadCurves = cubic2quad( 185 | x, y, 186 | segment[1], segment[2], 187 | segment[3], segment[4], 188 | segment[5], segment[6], 189 | accuracy 190 | ); 191 | 192 | var res = []; 193 | 194 | for (var i = 2; i < quadCurves.length; i += 4) { 195 | res.push([ 'Q', quadCurves[i], quadCurves[i + 1], quadCurves[i + 2], quadCurves[i + 3] ]); 196 | } 197 | return res; 198 | } 199 | } 200 | 201 | 202 | // Converts svg points to contours. All points must be converted 203 | // to relative ones, smooth curves must be converted to generic ones 204 | // before this conversion. 205 | // 206 | function toSfntCoutours(svgPath) { 207 | var resContours = []; 208 | var resContour = []; 209 | 210 | svgPath.iterate(function (segment, index, x, y) { 211 | 212 | //start new contour 213 | if (index === 0 || segment[0] === 'M') { 214 | resContour = []; 215 | resContours.push(resContour); 216 | } 217 | 218 | var name = segment[0]; 219 | 220 | if (name === 'Q') { 221 | //add control point of quad spline, it is not on curve 222 | resContour.push({ x: segment[1], y: segment[2], onCurve: false }); 223 | } 224 | 225 | // add on-curve point 226 | if (name === 'H') { 227 | // vertical line has Y coordinate only, X remains the same 228 | resContour.push({ x: segment[1], y: y, onCurve: true }); 229 | } else if (name === 'V') { 230 | // horizontal line has X coordinate only, Y remains the same 231 | resContour.push({ x: x, y: segment[1], onCurve: true }); 232 | } else if (name !== 'Z') { 233 | // for all commands (except H and V) X and Y are placed in the end of the segment 234 | resContour.push({ x: segment[segment.length - 2], y: segment[segment.length - 1], onCurve: true }); 235 | } 236 | 237 | }); 238 | return resContours; 239 | } 240 | 241 | 242 | module.exports.load = load; 243 | module.exports.cubicToQuad = cubicToQuad; 244 | module.exports.toSfntCoutours = toSfntCoutours; 245 | -------------------------------------------------------------------------------- /lib/ttf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var ByteBuffer = require('microbuffer'); 5 | 6 | var createGSUBTable = require('./ttf/tables/gsub'); 7 | var createOS2Table = require('./ttf/tables/os2'); 8 | var createCMapTable = require('./ttf/tables/cmap'); 9 | var createGlyfTable = require('./ttf/tables/glyf'); 10 | var createHeadTable = require('./ttf/tables/head'); 11 | var createHHeadTable = require('./ttf/tables/hhea'); 12 | var createHtmxTable = require('./ttf/tables/hmtx'); 13 | var createLocaTable = require('./ttf/tables/loca'); 14 | var createMaxpTable = require('./ttf/tables/maxp'); 15 | var createNameTable = require('./ttf/tables/name'); 16 | var createPostTable = require('./ttf/tables/post'); 17 | 18 | var utils = require('./ttf/utils'); 19 | 20 | // Tables 21 | var TABLES = [ 22 | { innerName: 'GSUB', order: 4, create: createGSUBTable }, // GSUB 23 | { innerName: 'OS/2', order: 4, create: createOS2Table }, // OS/2 24 | { innerName: 'cmap', order: 6, create: createCMapTable }, // cmap 25 | { innerName: 'glyf', order: 8, create: createGlyfTable }, // glyf 26 | { innerName: 'head', order: 2, create: createHeadTable }, // head 27 | { innerName: 'hhea', order: 1, create: createHHeadTable }, // hhea 28 | { innerName: 'hmtx', order: 5, create: createHtmxTable }, // hmtx 29 | { innerName: 'loca', order: 7, create: createLocaTable }, // loca 30 | { innerName: 'maxp', order: 3, create: createMaxpTable }, // maxp 31 | { innerName: 'name', order: 9, create: createNameTable }, // name 32 | { innerName: 'post', order: 10, create: createPostTable } // post 33 | ]; 34 | 35 | // Various constants 36 | var CONST = { 37 | VERSION: 0x10000, 38 | CHECKSUM_ADJUSTMENT: 0xB1B0AFBA 39 | }; 40 | 41 | function ulong(t) { 42 | t &= 0xffffffff; 43 | if (t < 0) { 44 | t += 0x100000000; 45 | } 46 | return t; 47 | } 48 | 49 | function calc_checksum(buf) { 50 | var sum = 0; 51 | var nlongs = Math.floor(buf.length / 4); 52 | var i; 53 | 54 | for (i = 0; i < nlongs; ++i) { 55 | var t = buf.getUint32(i * 4); 56 | 57 | sum = ulong(sum + t); 58 | } 59 | 60 | var leftBytes = buf.length - nlongs * 4; //extra 1..3 bytes found, because table is not aligned. Need to include them in checksum too. 61 | 62 | if (leftBytes > 0) { 63 | var leftRes = 0; 64 | 65 | for (i = 0; i < 4; i++) { 66 | leftRes = (leftRes << 8) + ((i < leftBytes) ? buf.getUint8(nlongs * 4 + i) : 0); 67 | } 68 | sum = ulong(sum + leftRes); 69 | } 70 | return sum; 71 | } 72 | 73 | function generateTTF(font) { 74 | 75 | // Prepare TTF contours objects. Note, that while sfnt countours are classes, 76 | // ttf contours are just plain arrays of points 77 | _.forEach(font.glyphs, function (glyph) { 78 | glyph.ttfContours = _.map(glyph.contours, function (contour) { 79 | return contour.points; 80 | }); 81 | }); 82 | 83 | // Process ttf contours data 84 | _.forEach(font.glyphs, function (glyph) { 85 | 86 | // 0.3px accuracy is ok. fo 1000x1000. 87 | glyph.ttfContours = utils.simplify(glyph.ttfContours, 0.3); 88 | glyph.ttfContours = utils.simplify(glyph.ttfContours, 0.3); // one pass is not enougth 89 | 90 | // Interpolated points can be removed. 1.1px is acceptable 91 | // measure - it will give us 1px error after coordinates rounding. 92 | glyph.ttfContours = utils.interpolate(glyph.ttfContours, 1.1); 93 | 94 | glyph.ttfContours = utils.roundPoints(glyph.ttfContours); 95 | glyph.ttfContours = utils.removeClosingReturnPoints(glyph.ttfContours); 96 | glyph.ttfContours = utils.toRelative(glyph.ttfContours); 97 | }); 98 | 99 | // Add tables 100 | var headerSize = 12 + 16 * TABLES.length; // TTF header plus table headers 101 | var bufSize = headerSize; 102 | 103 | _.forEach(TABLES, function (table) { 104 | //store each table in its own buffer 105 | table.buffer = table.create(font); 106 | table.length = table.buffer.length; 107 | table.corLength = table.length + (4 - table.length % 4) % 4; // table size should be divisible to 4 108 | table.checkSum = calc_checksum(table.buffer); 109 | bufSize += table.corLength; 110 | }); 111 | 112 | //calculate offsets 113 | var offset = headerSize; 114 | 115 | _.forEach(_.sortBy(TABLES, 'order'), function (table) { 116 | table.offset = offset; 117 | offset += table.corLength; 118 | }); 119 | 120 | //create TTF buffer 121 | 122 | var buf = new ByteBuffer(bufSize); 123 | 124 | //special constants 125 | var entrySelector = Math.floor(Math.log(TABLES.length) / Math.LN2); 126 | var searchRange = Math.pow(2, entrySelector) * 16; 127 | var rangeShift = TABLES.length * 16 - searchRange; 128 | 129 | // Add TTF header 130 | buf.writeUint32(CONST.VERSION); 131 | buf.writeUint16(TABLES.length); 132 | buf.writeUint16(searchRange); 133 | buf.writeUint16(entrySelector); 134 | buf.writeUint16(rangeShift); 135 | 136 | _.forEach(TABLES, function (table) { 137 | buf.writeUint32(utils.identifier(table.innerName)); //inner name 138 | buf.writeUint32(table.checkSum); //checksum 139 | buf.writeUint32(table.offset); //offset 140 | buf.writeUint32(table.length); //length 141 | }); 142 | 143 | var headOffset = 0; 144 | 145 | _.forEach(_.sortBy(TABLES, 'order'), function (table) { 146 | if (table.innerName === 'head') { //we must store head offset to write font checksum 147 | headOffset = buf.tell(); 148 | } 149 | buf.writeBytes(table.buffer.buffer); 150 | for (var i = table.length; i < table.corLength; i++) { //align table to be divisible to 4 151 | buf.writeUint8(0); 152 | } 153 | }); 154 | 155 | // Write font checksum (corrected by magic value) into HEAD table 156 | buf.setUint32(headOffset + 8, ulong(CONST.CHECKSUM_ADJUSTMENT - calc_checksum(buf))); 157 | 158 | return buf; 159 | } 160 | 161 | module.exports = generateTTF; 162 | -------------------------------------------------------------------------------- /lib/ttf/tables/cmap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/cmap.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | 8 | function getIDByUnicode(font, unicode) { 9 | return font.codePoints[unicode] ? font.codePoints[unicode].id : 0; 10 | } 11 | 12 | // Calculate character segments with non-interruptable chains of unicodes 13 | function getSegments(font, bounds) { 14 | bounds = bounds || Number.MAX_VALUE; 15 | 16 | var result = []; 17 | var segment; 18 | 19 | // prevEndCode only changes when a segment closes 20 | _.forEach(font.codePoints, function (glyph, unicode) { 21 | unicode = parseInt(unicode, 10); 22 | if (unicode >= bounds) { 23 | return false; 24 | } 25 | // Initialize first segment or add new segment if code "hole" is found 26 | if (!segment || unicode !== (segment.end + 1)) { 27 | if (segment) { 28 | result.push(segment); 29 | } 30 | segment = { 31 | start: unicode 32 | }; 33 | } 34 | segment.end = unicode; 35 | }); 36 | 37 | // Need to finish the last segment 38 | if (segment) { 39 | result.push(segment); 40 | } 41 | 42 | _.forEach(result, function (segment) { 43 | segment.length = segment.end - segment.start + 1; 44 | }); 45 | 46 | return result; 47 | } 48 | 49 | // Returns an array of {unicode, glyph} sets for all valid code points up to bounds 50 | function getCodePoints(codePoints, bounds) { 51 | bounds = bounds || Number.MAX_VALUE; 52 | 53 | var result = []; 54 | 55 | _.forEach(codePoints, function (glyph, unicode) { 56 | unicode = parseInt(unicode, 10); 57 | // Since this is a sparse array, iterating will only yield the valid code points 58 | if (unicode > bounds) { 59 | return false; 60 | } 61 | result.push({ 62 | unicode: unicode, 63 | glyph: glyph 64 | }); 65 | }); 66 | return result; 67 | } 68 | 69 | function bufferForTable(format, length) { 70 | var fieldWidth = format === 8 || format === 10 || format === 12 || format === 13 ? 4 : 2; 71 | 72 | length += (0 73 | + fieldWidth // Format 74 | + fieldWidth // Length 75 | + fieldWidth // Language 76 | ); 77 | 78 | var LANGUAGE = 0; 79 | var buffer = new ByteBuffer(length); 80 | 81 | var writer = fieldWidth === 4 ? buffer.writeUint32 : buffer.writeUint16; 82 | 83 | // Format specifier 84 | buffer.writeUint16(format); 85 | if (fieldWidth === 4) { 86 | // In case of formats 8.…, 10.…, 12.… and 13.…, this is the decimal part of the format number 87 | // But since have not been any point releases, this can be zero in that case as well 88 | buffer.writeUint16(0); 89 | } 90 | // Length 91 | writer.call(buffer, length); 92 | // Language code (0, only used for legacy quickdraw tables) 93 | writer.call(buffer, LANGUAGE); 94 | 95 | return buffer; 96 | } 97 | 98 | function createFormat0Table(font) { 99 | var FORMAT = 0; 100 | 101 | var i; 102 | 103 | var length = 0xff + 1; //Format 0 maps only single-byte code points 104 | 105 | var buffer = bufferForTable(FORMAT, length); 106 | 107 | for (i = 0; i < length; i++) { 108 | buffer.writeUint8(getIDByUnicode(font, i)); // existing char in table 0..255 109 | } 110 | return buffer; 111 | } 112 | 113 | function createFormat4Table(font) { 114 | var FORMAT = 4; 115 | 116 | var i; 117 | 118 | var segments = getSegments(font, 0xFFFF); 119 | var glyphIndexArrays = []; 120 | 121 | _.forEach(segments, function (segment) { 122 | var glyphIndexArray = []; 123 | 124 | for (var unicode = segment.start; unicode <= segment.end; unicode++) { 125 | glyphIndexArray.push(getIDByUnicode(font, unicode)); 126 | } 127 | glyphIndexArrays.push(glyphIndexArray); 128 | }); 129 | 130 | var segCount = segments.length + 1; // + 1 for the 0xFFFF section 131 | var glyphIndexArrayLength = _.reduce(_.map(glyphIndexArrays, 'length'), function (result, count) { return result + count; }, 0); 132 | 133 | var length = (0 134 | + 2 // segCountX2 135 | + 2 // searchRange 136 | + 2 // entrySelector 137 | + 2 // rangeShift 138 | + 2 * segCount // endCodes 139 | + 2 // Padding 140 | + 2 * segCount //startCodes 141 | + 2 * segCount //idDeltas 142 | + 2 * segCount //idRangeOffsets 143 | + 2 * glyphIndexArrayLength 144 | ); 145 | 146 | var buffer = bufferForTable(FORMAT, length); 147 | 148 | buffer.writeUint16(segCount * 2); // segCountX2 149 | var maxExponent = Math.floor(Math.log(segCount) / Math.LN2); 150 | var searchRange = 2 * Math.pow(2, maxExponent); 151 | 152 | buffer.writeUint16(searchRange); // searchRange 153 | buffer.writeUint16(maxExponent); // entrySelector 154 | buffer.writeUint16(2 * segCount - searchRange); // rangeShift 155 | 156 | // Array of end counts 157 | _.forEach(segments, function (segment) { 158 | buffer.writeUint16(segment.end); 159 | }); 160 | buffer.writeUint16(0xFFFF); // endCountArray should be finished with 0xFFFF 161 | 162 | buffer.writeUint16(0); // reservedPad 163 | 164 | // Array of start counts 165 | _.forEach(segments, function (segment) { 166 | buffer.writeUint16(segment.start); //startCountArray 167 | }); 168 | buffer.writeUint16(0xFFFF); // startCountArray should be finished with 0xFFFF 169 | 170 | // Array of deltas. Leave it zero to not complicate things when using the glyph index array 171 | for (i = 0; i < segments.length; i++) { 172 | buffer.writeUint16(0); // delta is always zero because we use the glyph array 173 | } 174 | buffer.writeUint16(1); // idDeltaArray should be finished with 1 175 | 176 | // Array of range offsets 177 | var offset = 0; 178 | 179 | for (i = 0; i < segments.length; i++) { 180 | buffer.writeUint16(2 * ((segments.length - i + 1) + offset)); 181 | offset += glyphIndexArrays[i].length; 182 | } 183 | buffer.writeUint16(0); // rangeOffsetArray should be finished with 0 184 | 185 | _.forEach(glyphIndexArrays, function (glyphIndexArray) { 186 | _.forEach(glyphIndexArray, function (glyphId) { 187 | buffer.writeUint16(glyphId); 188 | }); 189 | }); 190 | 191 | return buffer; 192 | } 193 | 194 | function createFormat12Table(font) { 195 | var FORMAT = 12; 196 | 197 | var codePoints = getCodePoints(font.codePoints); 198 | 199 | var length = (0 200 | + 4 // nGroups 201 | + 4 * codePoints.length // startCharCode 202 | + 4 * codePoints.length // endCharCode 203 | + 4 * codePoints.length // startGlyphCode 204 | ); 205 | 206 | var buffer = bufferForTable(FORMAT, length); 207 | 208 | buffer.writeUint32(codePoints.length); // nGroups 209 | _.forEach(codePoints, function (codePoint) { 210 | buffer.writeUint32(codePoint.unicode); // startCharCode 211 | buffer.writeUint32(codePoint.unicode); // endCharCode 212 | buffer.writeUint32(codePoint.glyph.id); // startGlyphCode 213 | }); 214 | 215 | return buffer; 216 | } 217 | 218 | function createCMapTable(font) { 219 | var TABLE_HEAD = (0 220 | + 2 // platform 221 | + 2 // encoding 222 | + 4 // offset 223 | ); 224 | 225 | var singleByteTable = createFormat0Table(font); 226 | var twoByteTable = createFormat4Table(font); 227 | var fourByteTable = createFormat12Table(font); 228 | 229 | // Subtable headers must be sorted by platformID, encodingID 230 | var tableHeaders = [ 231 | // subtable 4, unicode 232 | { 233 | platformID: 0, 234 | encodingID: 3, 235 | table: twoByteTable 236 | }, 237 | // subtable 12, unicode 238 | { 239 | platformID: 0, 240 | encodingID: 4, 241 | table: fourByteTable 242 | }, 243 | // subtable 0, mac standard 244 | { 245 | platformID: 1, 246 | encodingID: 0, 247 | table: singleByteTable 248 | }, 249 | // subtable 4, windows standard, identical to the unicode table 250 | { 251 | platformID: 3, 252 | encodingID: 1, 253 | table: twoByteTable 254 | }, 255 | // subtable 12, windows ucs4 256 | { 257 | platformID: 3, 258 | encodingID: 10, 259 | table: fourByteTable 260 | } 261 | ]; 262 | 263 | var tables = [ 264 | twoByteTable, 265 | singleByteTable, 266 | fourByteTable 267 | ]; 268 | 269 | var tableOffset = (0 270 | + 2 // version 271 | + 2 // number of subtable headers 272 | + tableHeaders.length * TABLE_HEAD 273 | ); 274 | 275 | // Calculate offsets for each table 276 | _.forEach(tables, function (table) { 277 | table._tableOffset = tableOffset; 278 | tableOffset += table.length; 279 | }); 280 | 281 | var length = tableOffset; 282 | 283 | var buffer = new ByteBuffer(length); 284 | 285 | // Write table header. 286 | buffer.writeUint16(0); // version 287 | buffer.writeUint16(tableHeaders.length); // count 288 | 289 | // Write subtable headers 290 | _.forEach(tableHeaders, function (header) { 291 | buffer.writeUint16(header.platformID); // platform 292 | buffer.writeUint16(header.encodingID); // encoding 293 | buffer.writeUint32(header.table._tableOffset); // offset 294 | }); 295 | 296 | // Write subtables 297 | _.forEach(tables, function (table) { 298 | buffer.writeBytes(table.buffer); 299 | }); 300 | 301 | return buffer; 302 | } 303 | 304 | module.exports = createCMapTable; 305 | -------------------------------------------------------------------------------- /lib/ttf/tables/glyf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/glyf.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | 8 | function getFlags(glyph) { 9 | var result = []; 10 | 11 | _.forEach(glyph.ttfContours, function (contour) { 12 | _.forEach(contour, function (point) { 13 | var flag = point.onCurve ? 1 : 0; 14 | 15 | if (point.x === 0) { 16 | flag += 16; 17 | } else { 18 | if (-0xFF <= point.x && point.x <= 0xFF) { 19 | flag += 2; // the corresponding x-coordinate is 1 byte long 20 | } 21 | if (point.x > 0 && point.x <= 0xFF) { 22 | flag += 16; // If x-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative 23 | } 24 | } 25 | if (point.y === 0) { 26 | flag += 32; 27 | } else { 28 | if (-0xFF <= point.y && point.y <= 0xFF) { 29 | flag += 4; // the corresponding y-coordinate is 1 byte long 30 | } 31 | if (point.y > 0 && point.y <= 0xFF) { 32 | flag += 32; // If y-Short Vector is set, this bit describes the sign of the value, with 1 equalling positive and 0 negative. 33 | } 34 | } 35 | result.push(flag); 36 | }); 37 | }); 38 | return result; 39 | } 40 | 41 | //repeating flags can be packed 42 | function compactFlags(flags) { 43 | var result = []; 44 | var prevFlag = -1; 45 | var firstRepeat = false; 46 | 47 | _.forEach(flags, function (flag) { 48 | if (prevFlag === flag) { 49 | if (firstRepeat) { 50 | result[result.length - 1] += 8; //current flag repeats previous one, need to set 3rd bit of previous flag and set 1 to the current one 51 | result.push(1); 52 | firstRepeat = false; 53 | } else { 54 | result[result.length - 1]++; //when flag is repeating second or more times, we need to increase the last flag value 55 | } 56 | } else { 57 | firstRepeat = true; 58 | prevFlag = flag; 59 | result.push(flag); 60 | } 61 | }); 62 | return result; 63 | } 64 | 65 | function getCoords(glyph, coordName) { 66 | var result = []; 67 | 68 | _.forEach(glyph.ttfContours, function (contour) { 69 | result.push.apply(result, _.map(contour, coordName)); 70 | }); 71 | return result; 72 | } 73 | 74 | function compactCoords(coords) { 75 | return _.filter(coords, function (coord) { 76 | return coord !== 0; 77 | }); 78 | } 79 | 80 | //calculates length of glyph data in GLYF table 81 | function glyphDataSize(glyph) { 82 | // Ignore glyphs without outlines. These will get a length of zero in the “loca” table 83 | if (!glyph.contours.length) { 84 | return 0; 85 | } 86 | 87 | var result = 12; //glyph fixed properties 88 | 89 | result += glyph.contours.length * 2; //add contours 90 | 91 | _.forEach(glyph.ttf_x, function (x) { 92 | //add 1 or 2 bytes for each coordinate depending of its size 93 | result += ((-0xFF <= x && x <= 0xFF)) ? 1 : 2; 94 | }); 95 | 96 | _.forEach(glyph.ttf_y, function (y) { 97 | //add 1 or 2 bytes for each coordinate depending of its size 98 | result += ((-0xFF <= y && y <= 0xFF)) ? 1 : 2; 99 | }); 100 | 101 | // Add flags length to glyph size. 102 | result += glyph.ttf_flags.length; 103 | 104 | if (result % 4 !== 0) { // glyph size must be divisible by 4. 105 | result += 4 - result % 4; 106 | } 107 | return result; 108 | } 109 | 110 | function tableSize(font) { 111 | var result = 0; 112 | 113 | _.forEach(font.glyphs, function (glyph) { 114 | glyph.ttf_size = glyphDataSize(glyph); 115 | result += glyph.ttf_size; 116 | }); 117 | font.ttf_glyph_size = result; //sum of all glyph lengths 118 | return result; 119 | } 120 | 121 | function createGlyfTable(font) { 122 | 123 | _.forEach(font.glyphs, function (glyph) { 124 | glyph.ttf_flags = getFlags(glyph); 125 | glyph.ttf_flags = compactFlags(glyph.ttf_flags); 126 | glyph.ttf_x = getCoords(glyph, 'x'); 127 | glyph.ttf_x = compactCoords(glyph.ttf_x); 128 | glyph.ttf_y = getCoords(glyph, 'y'); 129 | glyph.ttf_y = compactCoords(glyph.ttf_y); 130 | }); 131 | 132 | var buf = new ByteBuffer(tableSize(font)); 133 | 134 | _.forEach(font.glyphs, function (glyph) { 135 | 136 | // Ignore glyphs without outlines. These will get a length of zero in the “loca” table 137 | if (!glyph.contours.length) { 138 | return; 139 | } 140 | 141 | var offset = buf.tell(); 142 | 143 | buf.writeInt16(glyph.contours.length); // numberOfContours 144 | buf.writeInt16(glyph.xMin); // xMin 145 | buf.writeInt16(glyph.yMin); // yMin 146 | buf.writeInt16(glyph.xMax); // xMax 147 | buf.writeInt16(glyph.yMax); // yMax 148 | 149 | // Array of end points 150 | var endPtsOfContours = -1; 151 | 152 | var ttfContours = glyph.ttfContours; 153 | 154 | _.forEach(ttfContours, function (contour) { 155 | endPtsOfContours += contour.length; 156 | buf.writeInt16(endPtsOfContours); 157 | }); 158 | 159 | buf.writeInt16(0); // instructionLength, is not used here 160 | 161 | // Array of flags 162 | _.forEach(glyph.ttf_flags, function (flag) { 163 | buf.writeInt8(flag); 164 | }); 165 | 166 | // Array of X relative coordinates 167 | _.forEach(glyph.ttf_x, function (x) { 168 | if (-0xFF <= x && x <= 0xFF) { 169 | buf.writeUint8(Math.abs(x)); 170 | } else { 171 | buf.writeInt16(x); 172 | } 173 | }); 174 | 175 | // Array of Y relative coordinates 176 | _.forEach(glyph.ttf_y, function (y) { 177 | if (-0xFF <= y && y <= 0xFF) { 178 | buf.writeUint8(Math.abs(y)); 179 | } else { 180 | buf.writeInt16(y); 181 | } 182 | }); 183 | 184 | var tail = (buf.tell() - offset) % 4; 185 | 186 | if (tail !== 0) { // glyph size must be divisible by 4. 187 | for (; tail < 4; tail++) { 188 | buf.writeUint8(0); 189 | } 190 | } 191 | }); 192 | return buf; 193 | } 194 | 195 | module.exports = createGlyfTable; 196 | -------------------------------------------------------------------------------- /lib/ttf/tables/gsub.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/GSUB.htm 4 | 5 | var _ = require('lodash'); 6 | var identifier = require('../utils.js').identifier; 7 | var ByteBuffer = require('microbuffer'); 8 | 9 | function createScript() { 10 | var scriptRecord = (0 11 | + 2 // Script DefaultLangSys Offset 12 | + 2 // Script[0] LangSysCount (0) 13 | ); 14 | 15 | var langSys = (0 16 | + 2 // Script DefaultLangSys LookupOrder 17 | + 2 // Script DefaultLangSys ReqFeatureIndex 18 | + 2 // Script DefaultLangSys FeatureCount (0?) 19 | + 2 // Script Optional Feature Index[0] 20 | ); 21 | 22 | var length = (0 23 | + scriptRecord 24 | + langSys 25 | ); 26 | 27 | var buffer = new ByteBuffer(length); 28 | 29 | // Script Record 30 | // Offset to the start of langSys from the start of scriptRecord 31 | buffer.writeUint16(scriptRecord); // DefaultLangSys 32 | 33 | // Number of LangSys entries other than the default (none) 34 | buffer.writeUint16(0); 35 | 36 | // LangSys record (DefaultLangSys) 37 | // LookupOrder 38 | buffer.writeUint16(0); 39 | // ReqFeatureIndex -> only one required feature: all ligatures 40 | buffer.writeUint16(0); 41 | // Number of FeatureIndex values for this language system (excludes the required feature) 42 | buffer.writeUint16(1); 43 | // FeatureIndex for the first optional feature 44 | // Note: Adding the same feature to both the optional 45 | // and the required features is a clear violation of the spec 46 | // but it fixes IE not displaying the ligatures. 47 | // See http://partners.adobe.com/public/developer/opentype/index_table_formats.html, Section “Language System Table” 48 | // “FeatureCount: Number of FeatureIndex values for this language system-*excludes the required feature*” (emphasis added) 49 | buffer.writeUint16(0); 50 | 51 | return buffer; 52 | } 53 | 54 | function createScriptList() { 55 | var scriptSize = (0 56 | + 4 // Tag 57 | + 2 // Offset 58 | ); 59 | 60 | // tags should be arranged alphabetically 61 | var scripts = [ 62 | [ 'DFLT', createScript() ], 63 | [ 'latn', createScript() ] 64 | ]; 65 | 66 | var header = (0 67 | + 2 // Script count 68 | + scripts.length * scriptSize 69 | ); 70 | 71 | var tableLengths = _.reduce(_.map(scripts, function (script) { return script[1].length; }), function (result, count) { return result + count; }, 0); 72 | 73 | var length = (0 74 | + header 75 | + tableLengths 76 | ); 77 | 78 | var buffer = new ByteBuffer(length); 79 | 80 | // Script count 81 | buffer.writeUint16(scripts.length); 82 | 83 | // Write all ScriptRecords 84 | var offset = header; 85 | 86 | _.forEach(scripts, function (script) { 87 | var name = script[0], table = script[1]; 88 | 89 | // Script identifier (DFLT/latn) 90 | buffer.writeUint32(identifier(name)); 91 | // Offset to the ScriptRecord from start of the script list 92 | buffer.writeUint16(offset); 93 | // Increment offset by script table length 94 | offset += table.length; 95 | }); 96 | 97 | // Write all ScriptTables 98 | _.forEach(scripts, function (script) { 99 | var table = script[1]; 100 | 101 | buffer.writeBytes(table.buffer); 102 | }); 103 | 104 | return buffer; 105 | } 106 | 107 | // Write one feature containing all ligatures 108 | function createFeatureList() { 109 | var header = (0 110 | + 2 // FeatureCount 111 | + 4 // FeatureTag[0] 112 | + 2 // Feature Offset[0] 113 | ); 114 | 115 | var length = (0 116 | + header 117 | + 2 // FeatureParams[0] 118 | + 2 // LookupCount[0] 119 | + 2 // Lookup[0] LookupListIndex[0] 120 | ); 121 | 122 | var buffer = new ByteBuffer(length); 123 | 124 | // FeatureCount 125 | buffer.writeUint16(1); 126 | // FeatureTag[0] 127 | buffer.writeUint32(identifier('liga')); 128 | // Feature Offset[0] 129 | buffer.writeUint16(header); 130 | // FeatureParams[0] 131 | buffer.writeUint16(0); 132 | // LookupCount[0] 133 | buffer.writeUint16(1); 134 | // Index into lookup table. Since we only have ligatures, the index is always 0 135 | buffer.writeUint16(0); 136 | 137 | return buffer; 138 | } 139 | 140 | function createLigatureCoverage(font, ligatureGroups) { 141 | var glyphCount = ligatureGroups.length; 142 | 143 | var length = (0 144 | + 2 // CoverageFormat 145 | + 2 // GlyphCount 146 | + 2 * glyphCount // GlyphID[i] 147 | ); 148 | 149 | var buffer = new ByteBuffer(length); 150 | 151 | // CoverageFormat 152 | buffer.writeUint16(1); 153 | 154 | // Length 155 | buffer.writeUint16(glyphCount); 156 | 157 | 158 | _.forEach(ligatureGroups, function (group) { 159 | buffer.writeUint16(group.startGlyph.id); 160 | }); 161 | 162 | return buffer; 163 | } 164 | 165 | function createLigatureTable(font, ligature) { 166 | var allCodePoints = font.codePoints; 167 | 168 | var unicode = ligature.unicode; 169 | 170 | var length = (0 171 | + 2 // LigGlyph 172 | + 2 // CompCount 173 | + 2 * (unicode.length - 1) 174 | ); 175 | 176 | var buffer = new ByteBuffer(length); 177 | 178 | // LigGlyph 179 | var glyph = ligature.glyph; 180 | 181 | buffer.writeUint16(glyph.id); 182 | 183 | // CompCount 184 | buffer.writeUint16(unicode.length); 185 | 186 | // Compound glyphs (excluding first as it’s already in the coverage table) 187 | for (var i = 1; i < unicode.length; i++) { 188 | glyph = allCodePoints[unicode[i]]; 189 | buffer.writeUint16(glyph.id); 190 | } 191 | 192 | return buffer; 193 | } 194 | 195 | function createLigatureSet(font, codePoint, ligatures) { 196 | var ligatureTables = []; 197 | 198 | _.forEach(ligatures, function (ligature) { 199 | ligatureTables.push(createLigatureTable(font, ligature)); 200 | }); 201 | 202 | var tableLengths = _.reduce(_.map(ligatureTables, 'length'), function (result, count) { return result + count; }, 0); 203 | 204 | var offset = (0 205 | + 2 // LigatureCount 206 | + 2 * ligatures.length 207 | ); 208 | 209 | var length = (0 210 | + offset 211 | + tableLengths 212 | ); 213 | 214 | var buffer = new ByteBuffer(length); 215 | 216 | // LigatureCount 217 | buffer.writeUint16(ligatures.length); 218 | 219 | // Ligature offsets 220 | _.forEach(ligatureTables, function (table) { 221 | // The offset to the current set, from SubstFormat 222 | buffer.writeUint16(offset); 223 | offset += table.length; 224 | }); 225 | 226 | // Ligatures 227 | _.forEach(ligatureTables, function (table) { 228 | buffer.writeBytes(table.buffer); 229 | }); 230 | 231 | return buffer; 232 | } 233 | 234 | function createLigatureList(font, ligatureGroups) { 235 | var sets = []; 236 | 237 | _.forEach(ligatureGroups, function (group) { 238 | var set = createLigatureSet(font, group.codePoint, group.ligatures); 239 | 240 | sets.push(set); 241 | }); 242 | 243 | var setLengths = _.reduce(_.map(sets, 'length'), function (result, count) { return result + count; }, 0); 244 | 245 | var coverage = createLigatureCoverage(font, ligatureGroups); 246 | 247 | var tableOffset = (0 248 | + 2 // Lookup type 249 | + 2 // Lokup flag 250 | + 2 // SubTableCount 251 | + 2 // SubTable[0] Offset 252 | ); 253 | 254 | var setOffset = (0 255 | + 2 // SubstFormat 256 | + 2 // Coverage offset 257 | + 2 // LigSetCount 258 | + 2 * sets.length // LigSet Offsets 259 | ); 260 | 261 | var coverageOffset = setOffset + setLengths; 262 | 263 | var length = (0 264 | + tableOffset 265 | + coverageOffset 266 | + coverage.length 267 | ); 268 | 269 | var buffer = new ByteBuffer(length); 270 | 271 | // Lookup type 4 – ligatures 272 | buffer.writeUint16(4); 273 | 274 | // Lookup flag – empty 275 | buffer.writeUint16(0); 276 | 277 | // Subtable count 278 | buffer.writeUint16(1); 279 | 280 | // Subtable[0] offset 281 | buffer.writeUint16(tableOffset); 282 | 283 | // SubstFormat 284 | buffer.writeUint16(1); 285 | 286 | // Coverage 287 | buffer.writeUint16(coverageOffset); 288 | 289 | // LigSetCount 290 | buffer.writeUint16(sets.length); 291 | 292 | _.forEach(sets, function (set) { 293 | // The offset to the current set, from SubstFormat 294 | buffer.writeUint16(setOffset); 295 | setOffset += set.length; 296 | }); 297 | 298 | _.forEach(sets, function (set) { 299 | buffer.writeBytes(set.buffer); 300 | }); 301 | 302 | buffer.writeBytes(coverage.buffer); 303 | 304 | return buffer; 305 | } 306 | 307 | // Add a lookup for each ligature 308 | function createLookupList(font) { 309 | var ligatures = font.ligatures; 310 | 311 | var groupedLigatures = {}; 312 | 313 | // Group ligatures by first code point 314 | _.forEach(ligatures, function (ligature) { 315 | var first = ligature.unicode[0]; 316 | 317 | if (!_.has(groupedLigatures, first)) { 318 | groupedLigatures[first] = []; 319 | } 320 | groupedLigatures[first].push(ligature); 321 | }); 322 | 323 | var ligatureGroups = []; 324 | 325 | _.forEach(groupedLigatures, function (ligatures, codePoint) { 326 | codePoint = parseInt(codePoint, 10); 327 | // Order ligatures by length, descending 328 | // “Ligatures with more components must be stored ahead of those with fewer components in order to be found” 329 | // From: http://partners.adobe.com/public/developer/opentype/index_tag7.html#liga 330 | ligatures.sort(function (ligA, ligB) { 331 | return ligB.unicode.length - ligA.unicode.length; 332 | }); 333 | ligatureGroups.push({ 334 | codePoint: codePoint, 335 | ligatures: ligatures, 336 | startGlyph: font.codePoints[codePoint] 337 | }); 338 | }); 339 | 340 | ligatureGroups.sort(function (a, b) { 341 | return a.startGlyph.id - b.startGlyph.id; 342 | }); 343 | 344 | var offset = (0 345 | + 2 // Lookup count 346 | + 2 // Lookup[0] offset 347 | ); 348 | 349 | var set = createLigatureList(font, ligatureGroups); 350 | 351 | var length = (0 352 | + offset 353 | + set.length 354 | ); 355 | 356 | var buffer = new ByteBuffer(length); 357 | 358 | // Lookup count 359 | buffer.writeUint16(1); 360 | 361 | // Lookup[0] offset 362 | buffer.writeUint16(offset); 363 | 364 | // Lookup[0] 365 | buffer.writeBytes(set.buffer); 366 | 367 | return buffer; 368 | } 369 | 370 | function createGSUB(font) { 371 | var scriptList = createScriptList(); 372 | var featureList = createFeatureList(); 373 | var lookupList = createLookupList(font); 374 | 375 | var lists = [ scriptList, featureList, lookupList ]; 376 | 377 | var offset = (0 378 | + 4 // Version 379 | + 2 * lists.length // List offsets 380 | ); 381 | 382 | // Calculate offsets 383 | _.forEach(lists, function (list) { 384 | list._listOffset = offset; 385 | offset += list.length; 386 | }); 387 | 388 | var length = offset; 389 | var buffer = new ByteBuffer(length); 390 | 391 | // Version 392 | buffer.writeUint32(0x00010000); 393 | 394 | // Offsets 395 | _.forEach(lists, function (list) { 396 | buffer.writeUint16(list._listOffset); 397 | }); 398 | 399 | // List contents 400 | _.forEach(lists, function (list) { 401 | buffer.writeBytes(list.buffer); 402 | }); 403 | 404 | return buffer; 405 | } 406 | 407 | module.exports = createGSUB; 408 | -------------------------------------------------------------------------------- /lib/ttf/tables/head.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/head.htm 4 | 5 | var ByteBuffer = require('microbuffer'); 6 | 7 | function dateToUInt64(date) { 8 | var startDate = new Date('1904-01-01T00:00:00.000Z'); 9 | 10 | return Math.floor((date - startDate) / 1000); 11 | } 12 | 13 | function createHeadTable(font) { 14 | 15 | var buf = new ByteBuffer(54); // fixed table length 16 | 17 | buf.writeInt32(0x10000); // version 18 | buf.writeInt32(font.revision * 0x10000); // fontRevision 19 | buf.writeUint32(0); // checkSumAdjustment 20 | buf.writeUint32(0x5F0F3CF5); // magicNumber 21 | // FLag meanings: 22 | // Bit 0: Baseline for font at y=0; 23 | // Bit 1: Left sidebearing point at x=0; 24 | // Bit 3: Force ppem to integer values for all internal scaler math; may use fractional ppem sizes if this bit is clear; 25 | buf.writeUint16(0x000B); // flags 26 | buf.writeUint16(font.unitsPerEm); // unitsPerEm 27 | buf.writeUint64(dateToUInt64(font.createdDate)); // created 28 | buf.writeUint64(dateToUInt64(font.modifiedDate)); // modified 29 | buf.writeInt16(font.xMin); // xMin 30 | buf.writeInt16(font.yMin); // yMin 31 | buf.writeInt16(font.xMax); // xMax 32 | buf.writeInt16(font.yMax); // yMax 33 | buf.writeUint16(font.macStyle); //macStyle 34 | buf.writeUint16(font.lowestRecPPEM); // lowestRecPPEM 35 | buf.writeInt16(2); // fontDirectionHint 36 | buf.writeInt16(font.ttf_glyph_size < 0x20000 ? 0 : 1); // indexToLocFormat, 0 for short offsets, 1 for long offsets 37 | buf.writeInt16(0); // glyphDataFormat 38 | 39 | return buf; 40 | } 41 | 42 | module.exports = createHeadTable; 43 | -------------------------------------------------------------------------------- /lib/ttf/tables/hhea.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/hhea.htm 4 | 5 | var ByteBuffer = require('microbuffer'); 6 | 7 | function createHHeadTable(font) { 8 | 9 | var buf = new ByteBuffer(36); // fixed table length 10 | 11 | buf.writeInt32(0x10000); // version 12 | buf.writeInt16(font.ascent); // ascent 13 | buf.writeInt16(font.descent); // descend 14 | // Non zero lineGap causes offset in IE, https://github.com/fontello/svg2ttf/issues/37 15 | buf.writeInt16(0); // lineGap 16 | buf.writeUint16(font.maxWidth); // advanceWidthMax 17 | buf.writeInt16(font.minLsb); // minLeftSideBearing 18 | buf.writeInt16(font.minRsb); // minRightSideBearing 19 | buf.writeInt16(font.maxExtent); // xMaxExtent 20 | buf.writeInt16(1); // caretSlopeRise 21 | buf.writeInt16(0); // caretSlopeRun 22 | buf.writeUint32(0); // reserved1 23 | buf.writeUint32(0); // reserved2 24 | buf.writeUint16(0); // reserved3 25 | buf.writeInt16(0); // metricDataFormat 26 | buf.writeUint16(font.glyphs.length); // numberOfHMetrics 27 | 28 | return buf; 29 | } 30 | 31 | module.exports = createHHeadTable; 32 | -------------------------------------------------------------------------------- /lib/ttf/tables/hmtx.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/hmtx.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | 8 | function createHtmxTable(font) { 9 | 10 | var buf = new ByteBuffer(font.glyphs.length * 4); 11 | 12 | _.forEach(font.glyphs, function (glyph) { 13 | buf.writeUint16(glyph.width); //advanceWidth 14 | buf.writeInt16(glyph.xMin); //lsb 15 | }); 16 | return buf; 17 | } 18 | 19 | module.exports = createHtmxTable; 20 | -------------------------------------------------------------------------------- /lib/ttf/tables/loca.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/loca.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | 8 | function tableSize(font, isShortFormat) { 9 | var result = (font.glyphs.length + 1) * (isShortFormat ? 2 : 4); // by glyph count + tail 10 | 11 | return result; 12 | } 13 | 14 | function createLocaTable(font) { 15 | 16 | var isShortFormat = font.ttf_glyph_size < 0x20000; 17 | 18 | var buf = new ByteBuffer(tableSize(font, isShortFormat)); 19 | 20 | var location = 0; 21 | 22 | // Array of offsets in GLYF table for each glyph 23 | _.forEach(font.glyphs, function (glyph) { 24 | if (isShortFormat) { 25 | buf.writeUint16(location); 26 | location += glyph.ttf_size / 2; // actual location must be divided to 2 in short format 27 | } else { 28 | buf.writeUint32(location); 29 | location += glyph.ttf_size; //actual location is stored as is in long format 30 | } 31 | }); 32 | 33 | // The last glyph location is stored to get last glyph length 34 | if (isShortFormat) { 35 | buf.writeUint16(location); 36 | } else { 37 | buf.writeUint32(location); 38 | } 39 | 40 | return buf; 41 | } 42 | 43 | module.exports = createLocaTable; 44 | -------------------------------------------------------------------------------- /lib/ttf/tables/maxp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/maxp.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | 8 | // Find max points in glyph TTF contours. 9 | function getMaxPoints(font) { 10 | return _.max(_.map(font.glyphs, function (glyph) { 11 | return _.reduce(glyph.ttfContours, function (sum, ctr) { return sum + ctr.length; }, 0); 12 | })); 13 | } 14 | 15 | function getMaxContours(font) { 16 | return _.max(_.map(font.glyphs, function (glyph) { 17 | return glyph.ttfContours.length; 18 | })); 19 | } 20 | 21 | function createMaxpTable(font) { 22 | 23 | var buf = new ByteBuffer(32); 24 | 25 | buf.writeInt32(0x10000); // version 26 | buf.writeUint16(font.glyphs.length); // numGlyphs 27 | buf.writeUint16(getMaxPoints(font)); // maxPoints 28 | buf.writeUint16(getMaxContours(font)); // maxContours 29 | buf.writeUint16(0); // maxCompositePoints 30 | buf.writeUint16(0); // maxCompositeContours 31 | buf.writeUint16(2); // maxZones 32 | buf.writeUint16(0); // maxTwilightPoints 33 | // It is unclear how to calculate maxStorage, maxFunctionDefs and maxInstructionDefs. 34 | // These are magic constants now, with values exceeding values from FontForge 35 | buf.writeUint16(10); // maxStorage 36 | buf.writeUint16(10); // maxFunctionDefs 37 | buf.writeUint16(0); // maxInstructionDefs 38 | buf.writeUint16(255); // maxStackElements 39 | buf.writeUint16(0); // maxSizeOfInstructions 40 | buf.writeUint16(0); // maxComponentElements 41 | buf.writeUint16(0); // maxComponentDepth 42 | 43 | return buf; 44 | } 45 | 46 | module.exports = createMaxpTable; 47 | -------------------------------------------------------------------------------- /lib/ttf/tables/name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/name.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | var Str = require('../../str'); 8 | 9 | var TTF_NAMES = { 10 | COPYRIGHT: 0, 11 | FONT_FAMILY: 1, 12 | ID: 3, 13 | DESCRIPTION: 10, 14 | URL_VENDOR: 11 15 | }; 16 | 17 | function tableSize(names) { 18 | var result = 6; // table header 19 | 20 | _.forEach(names, function (name) { 21 | result += 12 + name.data.length; //name header and data 22 | }); 23 | return result; 24 | } 25 | 26 | function getStrings(name, id) { 27 | var result = []; 28 | var str = new Str(name); 29 | 30 | result.push({ data: str.toUTF8Bytes(), id: id, platformID : 1, encodingID : 0, languageID : 0 }); //mac standard 31 | result.push({ data: str.toUCS2Bytes(), id: id, platformID : 3, encodingID : 1, languageID : 0x409 }); //windows standard 32 | return result; 33 | } 34 | 35 | // Collect font names 36 | function getNames(font) { 37 | var result = []; 38 | 39 | if (font.copyright) { 40 | result.push.apply(result, getStrings(font.copyright, TTF_NAMES.COPYRIGHT)); 41 | } 42 | if (font.familyName) { 43 | result.push.apply(result, getStrings(font.familyName, TTF_NAMES.FONT_FAMILY)); 44 | } 45 | if (font.id) { 46 | result.push.apply(result, getStrings(font.id, TTF_NAMES.ID)); 47 | } 48 | result.push.apply(result, getStrings(font.description, TTF_NAMES.DESCRIPTION)); 49 | result.push.apply(result, getStrings(font.url, TTF_NAMES.URL_VENDOR)); 50 | 51 | _.forEach(font.sfntNames, function (sfntName) { 52 | result.push.apply(result, getStrings(sfntName.value, sfntName.id)); 53 | }); 54 | 55 | result.sort(function (a, b) { 56 | var orderFields = [ 'platformID', 'encodingID', 'languageID', 'id' ]; 57 | var i; 58 | 59 | for (i = 0; i < orderFields.length; i++) { 60 | if (a[orderFields[i]] !== b[orderFields[i]]) { 61 | return a[orderFields[i]] < b[orderFields[i]] ? -1 : 1; 62 | } 63 | } 64 | return 0; 65 | }); 66 | 67 | return result; 68 | } 69 | 70 | function createNameTable(font) { 71 | 72 | var names = getNames(font); 73 | 74 | var buf = new ByteBuffer(tableSize(names)); 75 | 76 | buf.writeUint16(0); // formatSelector 77 | buf.writeUint16(names.length); // nameRecordsCount 78 | var offsetPosition = buf.tell(); 79 | 80 | buf.writeUint16(0); // offset, will be filled later 81 | var nameOffset = 0; 82 | 83 | _.forEach(names, function (name) { 84 | buf.writeUint16(name.platformID); // platformID 85 | buf.writeUint16(name.encodingID); // platEncID 86 | buf.writeUint16(name.languageID); // languageID, English (USA) 87 | buf.writeUint16(name.id); // nameID 88 | buf.writeUint16(name.data.length); // reclength 89 | buf.writeUint16(nameOffset); // offset 90 | nameOffset += name.data.length; 91 | }); 92 | var actualStringDataOffset = buf.tell(); 93 | 94 | //Array of bytes with actual string data 95 | _.forEach(names, function (name) { 96 | buf.writeBytes(name.data); 97 | }); 98 | 99 | //write actual string data offset 100 | buf.seek(offsetPosition); 101 | buf.writeUint16(actualStringDataOffset); // offset 102 | 103 | return buf; 104 | } 105 | 106 | module.exports = createNameTable; 107 | -------------------------------------------------------------------------------- /lib/ttf/tables/os2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/os2.htm 4 | 5 | var _ = require('lodash'); 6 | var identifier = require('../utils.js').identifier; 7 | var ByteBuffer = require('microbuffer'); 8 | 9 | //get first glyph unicode 10 | function getFirstCharIndex(font) { 11 | return Math.max(0, Math.min(0xffff, Math.abs(_.minBy(Object.keys(font.codePoints), function (point) { 12 | return parseInt(point, 10); 13 | })))); 14 | } 15 | 16 | //get last glyph unicode 17 | function getLastCharIndex(font) { 18 | return Math.max(0, Math.min(0xffff, Math.abs(_.maxBy(Object.keys(font.codePoints), function (point) { 19 | return parseInt(point, 10); 20 | })))); 21 | } 22 | 23 | // OpenType spec: https://docs.microsoft.com/en-us/typography/opentype/spec/os2 24 | function createOS2Table(font) { 25 | 26 | // use at least 2 for ligatures and kerning 27 | var maxContext = font.ligatures.map(function (l) { 28 | return l.unicode.length; 29 | }).reduce(function (a, b) { 30 | return Math.max(a, b); 31 | }, 2); 32 | 33 | var buf = new ByteBuffer(96); 34 | 35 | // Version 5 is not supported in the Android 5 browser. 36 | buf.writeUint16(4); // version 37 | buf.writeInt16(font.avgWidth); // xAvgCharWidth 38 | buf.writeUint16(font.weightClass); // usWeightClass 39 | buf.writeUint16(font.widthClass); // usWidthClass 40 | buf.writeInt16(font.fsType); // fsType 41 | buf.writeInt16(font.ySubscriptXSize); // ySubscriptXSize 42 | buf.writeInt16(font.ySubscriptYSize); //ySubscriptYSize 43 | buf.writeInt16(font.ySubscriptXOffset); // ySubscriptXOffset 44 | buf.writeInt16(font.ySubscriptYOffset); // ySubscriptYOffset 45 | buf.writeInt16(font.ySuperscriptXSize); // ySuperscriptXSize 46 | buf.writeInt16(font.ySuperscriptYSize); // ySuperscriptYSize 47 | buf.writeInt16(font.ySuperscriptXOffset); // ySuperscriptXOffset 48 | buf.writeInt16(font.ySuperscriptYOffset); // ySuperscriptYOffset 49 | buf.writeInt16(font.yStrikeoutSize); // yStrikeoutSize 50 | buf.writeInt16(font.yStrikeoutPosition); // yStrikeoutPosition 51 | buf.writeInt16(font.familyClass); // sFamilyClass 52 | buf.writeUint8(font.panose.familyType); // panose.bFamilyType 53 | buf.writeUint8(font.panose.serifStyle); // panose.bSerifStyle 54 | buf.writeUint8(font.panose.weight); // panose.bWeight 55 | buf.writeUint8(font.panose.proportion); // panose.bProportion 56 | buf.writeUint8(font.panose.contrast); // panose.bContrast 57 | buf.writeUint8(font.panose.strokeVariation); // panose.bStrokeVariation 58 | buf.writeUint8(font.panose.armStyle); // panose.bArmStyle 59 | buf.writeUint8(font.panose.letterform); // panose.bLetterform 60 | buf.writeUint8(font.panose.midline); // panose.bMidline 61 | buf.writeUint8(font.panose.xHeight); // panose.bXHeight 62 | // TODO: This field is used to specify the Unicode blocks or ranges based on the 'cmap' table. 63 | buf.writeUint32(0); // ulUnicodeRange1 64 | buf.writeUint32(0); // ulUnicodeRange2 65 | buf.writeUint32(0); // ulUnicodeRange3 66 | buf.writeUint32(0); // ulUnicodeRange4 67 | buf.writeUint32(identifier('PfEd')); // achVendID, equal to PfEd 68 | buf.writeUint16(font.fsSelection); // fsSelection 69 | buf.writeUint16(getFirstCharIndex(font)); // usFirstCharIndex 70 | buf.writeUint16(getLastCharIndex(font)); // usLastCharIndex 71 | buf.writeInt16(font.ascent); // sTypoAscender 72 | buf.writeInt16(font.descent); // sTypoDescender 73 | buf.writeInt16(font.lineGap); // lineGap 74 | // Enlarge win acscent/descent to avoid clipping 75 | // WinAscent - WinDecent should at least be equal to TypoAscender - TypoDescender + TypoLineGap: 76 | // https://www.high-logic.com/font-editor/fontcreator/tutorials/font-metrics-vertical-line-spacing 77 | buf.writeInt16(Math.max(font.yMax, font.ascent + font.lineGap)); // usWinAscent 78 | buf.writeInt16(-Math.min(font.yMin, font.descent)); // usWinDescent 79 | buf.writeInt32(1); // ulCodePageRange1, Latin 1 80 | buf.writeInt32(0); // ulCodePageRange2 81 | buf.writeInt16(font.xHeight); // sxHeight 82 | buf.writeInt16(font.capHeight); // sCapHeight 83 | buf.writeUint16(0); // usDefaultChar, pointing to missing glyph (always id=0) 84 | buf.writeUint16(0); // usBreakChar, code=32 isn't guaranteed to be a space in icon fonts 85 | buf.writeUint16(maxContext); // usMaxContext, use at least 2 for ligatures and kerning 86 | 87 | return buf; 88 | } 89 | 90 | module.exports = createOS2Table; 91 | -------------------------------------------------------------------------------- /lib/ttf/tables/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // See documentation here: http://www.microsoft.com/typography/otspec/post.htm 4 | 5 | var _ = require('lodash'); 6 | var ByteBuffer = require('microbuffer'); 7 | 8 | function tableSize(font, names) { 9 | var result = 36; // table header 10 | 11 | result += font.glyphs.length * 2; // name declarations 12 | _.forEach(names, function (name) { 13 | result += name.length; 14 | }); 15 | return result; 16 | } 17 | 18 | function pascalString(str) { 19 | var bytes = []; 20 | var len = str ? (str.length < 256 ? str.length : 255) : 0; //length in Pascal string is limited with 255 21 | 22 | bytes.push(len); 23 | for (var i = 0; i < len; i++) { 24 | var char = str.charCodeAt(i); 25 | 26 | bytes.push(char < 128 ? char : 95); //non-ASCII characters are substituted with '_' 27 | } 28 | return bytes; 29 | } 30 | 31 | function createPostTable(font) { 32 | 33 | var names = []; 34 | 35 | _.forEach(font.glyphs, function (glyph) { 36 | if (glyph.unicode !== 0) { 37 | names.push(pascalString(glyph.name)); 38 | } 39 | }); 40 | 41 | var buf = new ByteBuffer(tableSize(font, names)); 42 | 43 | buf.writeInt32(0x20000); // formatType, version 2.0 44 | buf.writeInt32(font.italicAngle); // italicAngle 45 | buf.writeInt16(font.underlinePosition); // underlinePosition 46 | buf.writeInt16(font.underlineThickness); // underlineThickness 47 | buf.writeUint32(font.isFixedPitch); // isFixedPitch 48 | buf.writeUint32(0); // minMemType42 49 | buf.writeUint32(0); // maxMemType42 50 | buf.writeUint32(0); // minMemType1 51 | buf.writeUint32(0); // maxMemType1 52 | buf.writeUint16(font.glyphs.length); // numberOfGlyphs 53 | 54 | // Array of glyph name indexes 55 | var index = 258; // first index of custom glyph name, it is calculated as glyph name index + 258 56 | 57 | _.forEach(font.glyphs, function (glyph) { 58 | if (glyph.unicode === 0) { 59 | buf.writeUint16(0);// missed element should have .notDef name in the Macintosh standard order. 60 | } else { 61 | buf.writeUint16(index++); 62 | } 63 | }); 64 | 65 | // Array of glyph name indexes 66 | _.forEach(names, function (name) { 67 | buf.writeBytes(name); 68 | }); 69 | 70 | return buf; 71 | } 72 | 73 | module.exports = createPostTable; 74 | -------------------------------------------------------------------------------- /lib/ttf/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var math = require('../math'); 5 | 6 | // Remove points, that looks like straight line 7 | function simplify(contours, accuracy) { 8 | return _.map(contours, function (contour) { 9 | var i, curr, prev, next; 10 | var p, pPrev, pNext; 11 | 12 | // run from the end, to simplify array elements removal 13 | for (i = contour.length - 2; i > 1; i--) { 14 | prev = contour[i - 1]; 15 | next = contour[i + 1]; 16 | curr = contour[i]; 17 | 18 | // skip point (both oncurve & offcurve), 19 | // if [prev,next] is straight line 20 | if (prev.onCurve && next.onCurve) { 21 | p = new math.Point(curr.x, curr.y); 22 | pPrev = new math.Point(prev.x, prev.y); 23 | pNext = new math.Point(next.x, next.y); 24 | if (math.isInLine(pPrev, p, pNext, accuracy)) { 25 | contour.splice(i, 1); 26 | } 27 | } 28 | } 29 | return contour; 30 | }); 31 | } 32 | 33 | // Remove interpolateable oncurve points 34 | // Those should be in the middle of nebor offcurve points 35 | function interpolate(contours, accuracy) { 36 | return _.map(contours, function (contour) { 37 | var resContour = []; 38 | 39 | _.forEach(contour, function (point, idx) { 40 | // Never skip first and last points 41 | if (idx === 0 || idx === (contour.length - 1)) { 42 | resContour.push(point); 43 | return; 44 | } 45 | 46 | var prev = contour[idx - 1]; 47 | var next = contour[idx + 1]; 48 | 49 | var p, pPrev, pNext; 50 | 51 | // skip interpolateable oncurve points (if exactly between previous and next offcurves) 52 | if (!prev.onCurve && point.onCurve && !next.onCurve) { 53 | p = new math.Point(point.x, point.y); 54 | pPrev = new math.Point(prev.x, prev.y); 55 | pNext = new math.Point(next.x, next.y); 56 | if (pPrev.add(pNext).div(2).sub(p).dist() < accuracy) { 57 | return; 58 | } 59 | } 60 | // keep the rest 61 | resContour.push(point); 62 | }); 63 | return resContour; 64 | }); 65 | } 66 | 67 | function roundPoints(contours) { 68 | return _.map(contours, function (contour) { 69 | return _.map(contour, function (point) { 70 | return { x: Math.round(point.x), y: Math.round(point.y), onCurve: point.onCurve }; 71 | }); 72 | }); 73 | } 74 | 75 | // Remove closing point if it is the same as first point of contour. 76 | // TTF doesn't need this point when drawing contours. 77 | function removeClosingReturnPoints(contours) { 78 | return _.map(contours, function (contour) { 79 | var length = contour.length; 80 | 81 | if (length > 1 && 82 | contour[0].x === contour[length - 1].x && 83 | contour[0].y === contour[length - 1].y) { 84 | contour.splice(length - 1); 85 | } 86 | return contour; 87 | }); 88 | } 89 | 90 | function toRelative(contours) { 91 | var prevPoint = { x: 0, y: 0 }; 92 | var resContours = []; 93 | var resContour; 94 | 95 | _.forEach(contours, function (contour) { 96 | resContour = []; 97 | resContours.push(resContour); 98 | _.forEach(contour, function (point) { 99 | resContour.push({ 100 | x: point.x - prevPoint.x, 101 | y: point.y - prevPoint.y, 102 | onCurve: point.onCurve 103 | }); 104 | prevPoint = point; 105 | }); 106 | }); 107 | return resContours; 108 | } 109 | 110 | function identifier(string, littleEndian) { 111 | var result = 0; 112 | 113 | for (var i = 0; i < string.length; i++) { 114 | result = result << 8; 115 | var index = littleEndian ? string.length - i - 1 : i; 116 | 117 | result += string.charCodeAt(index); 118 | } 119 | 120 | return result; 121 | } 122 | 123 | module.exports.interpolate = interpolate; 124 | module.exports.simplify = simplify; 125 | module.exports.roundPoints = roundPoints; 126 | module.exports.removeClosingReturnPoints = removeClosingReturnPoints; 127 | module.exports.toRelative = toRelative; 128 | module.exports.identifier = identifier; 129 | 130 | -------------------------------------------------------------------------------- /lib/ucs2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | // Taken from the punycode library 6 | function ucs2encode(array) { 7 | return _.map(array, function (value) { 8 | var output = ''; 9 | 10 | if (value > 0xFFFF) { 11 | value -= 0x10000; 12 | output += String.fromCharCode(value >>> 10 & 0x3FF | 0xD800); 13 | value = 0xDC00 | value & 0x3FF; 14 | } 15 | output += String.fromCharCode(value); 16 | return output; 17 | }).join(''); 18 | } 19 | 20 | function ucs2decode(string) { 21 | var output = [], 22 | counter = 0, 23 | length = string.length, 24 | value, 25 | extra; 26 | 27 | while (counter < length) { 28 | value = string.charCodeAt(counter++); 29 | if (value >= 0xD800 && value <= 0xDBFF && counter < length) { 30 | // high surrogate, and there is a next character 31 | extra = string.charCodeAt(counter++); 32 | if ((extra & 0xFC00) === 0xDC00) { // low surrogate 33 | output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); 34 | } else { 35 | // unmatched surrogate; only append this code unit, in case the next 36 | // code unit is the high surrogate of a surrogate pair 37 | output.push(value); 38 | counter--; 39 | } 40 | } else { 41 | output.push(value); 42 | } 43 | } 44 | return output; 45 | } 46 | 47 | module.exports = { 48 | encode: ucs2encode, 49 | decode: ucs2decode 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svg2ttf", 3 | "version": "6.0.3", 4 | "description": "Converts SVG font to TTF font", 5 | "keywords": [ 6 | "font", 7 | "ttf", 8 | "svg", 9 | "convertor" 10 | ], 11 | "author": "Sergey Batishchev ", 12 | "license": "MIT", 13 | "repository": "fontello/svg2ttf", 14 | "bin": { 15 | "svg2ttf": "./svg2ttf.js" 16 | }, 17 | "files": [ 18 | "index.js", 19 | "svg2ttf.js", 20 | "lib/" 21 | ], 22 | "scripts": { 23 | "lint": "eslint .", 24 | "test": "npm run lint && mocha", 25 | "update_fixture": "./svg2ttf.js --ts 1457357570703 test/fixtures/test.svg test/fixtures/test.ttf" 26 | }, 27 | "dependencies": { 28 | "argparse": "^2.0.1", 29 | "cubic2quad": "^1.2.1", 30 | "lodash": "^4.17.10", 31 | "microbuffer": "^1.0.0", 32 | "svgpath": "^2.1.5", 33 | "@xmldom/xmldom": "^0.7.2" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^7.0.0", 37 | "mocha": "^8.3.2", 38 | "opentype.js": "^1.3.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /svg2ttf.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Author: Sergey Batishchev 4 | 5 | Written for fontello.com project. 6 | */ 7 | 8 | /*eslint-disable no-console*/ 9 | 10 | 'use strict'; 11 | 12 | 13 | var fs = require('fs'); 14 | var ArgumentParser = require('argparse').ArgumentParser; 15 | 16 | var svg2ttf = require('./'); 17 | 18 | 19 | var parser = new ArgumentParser({ 20 | add_help: true, 21 | description: 'SVG to TTF font converter' 22 | }); 23 | 24 | parser.add_argument('-v', '--version', { 25 | action: 'version', 26 | version: require('./package.json').version 27 | }); 28 | 29 | parser.add_argument('-c', '--copyright', { 30 | help: 'Copyright text', 31 | required: false 32 | }); 33 | 34 | parser.add_argument('-d', '--description', { 35 | help: 'Override default description text', 36 | required: false, 37 | type: 'str' 38 | }); 39 | 40 | parser.add_argument('--ts', { 41 | help: 'Override font creation time (Unix time stamp)', 42 | required: false, 43 | type: 'int' 44 | }); 45 | 46 | parser.add_argument('-u', '--url', { 47 | help: 'Override default manufacturer url', 48 | required: false, 49 | type: 'str' 50 | }); 51 | 52 | parser.add_argument('--vs', { 53 | help: 'Override default font version string (Version 1.0), can be "x.y" or "Version x.y"', 54 | required: false, 55 | type: 'str' 56 | }); 57 | 58 | parser.add_argument('infile', { 59 | nargs: 1, 60 | help: 'Input file' 61 | }); 62 | 63 | parser.add_argument('outfile', { 64 | nargs: 1, 65 | help: 'Output file' 66 | }); 67 | 68 | 69 | var args = parser.parse_args(); 70 | var svg; 71 | var options = {}; 72 | 73 | try { 74 | svg = fs.readFileSync(args.infile[0], 'utf-8'); 75 | } catch (e) { 76 | console.error("Can't open input file (%s)", args.infile[0]); 77 | process.exit(1); 78 | } 79 | 80 | if (args.copyright) options.copyright = args.copyright; 81 | 82 | if (args.description) options.description = args.description; 83 | 84 | if (args.ts !== null) options.ts = args.ts; 85 | 86 | if (args.url) options.url = args.url; 87 | 88 | if (args.vs) options.version = args.vs; 89 | 90 | var result = Buffer.from ? 91 | Buffer.from(svg2ttf(svg, options).buffer) 92 | : 93 | new Buffer(svg2ttf(svg, options).buffer); 94 | 95 | fs.writeFileSync(args.outfile[0], result); 96 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /*global it, describe*/ 2 | 'use strict'; 3 | 4 | 5 | const assert = require('assert'); 6 | const opentype = require('opentype.js'); 7 | const svg2ttf = require('../'); 8 | 9 | const fixture = ` 10 | 11 | 12 | 13 | Copyright (C) 2016 by original authors @ fontello.com 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | `; 24 | 25 | 26 | describe('svg2ttf', function () { 27 | describe('version', function () { 28 | it('should throw on bad version value', function () { 29 | assert.throws(() => svg2ttf(fixture, { version: 123 })); 30 | assert.throws(() => svg2ttf(fixture, { version: 'abc' })); 31 | }); 32 | 33 | it('should set proper version', function () { 34 | let options, parsed; 35 | 36 | options = { version: '1.0' }; 37 | parsed = opentype.parse(svg2ttf(fixture, options).buffer.buffer); 38 | assert.strictEqual(parsed.tables.name.version.en, 'Version 1.0'); 39 | 40 | options = { version: 'Version 1.0' }; 41 | parsed = opentype.parse(svg2ttf(fixture, options).buffer.buffer); 42 | assert.strictEqual(parsed.tables.name.version.en, 'Version 1.0'); 43 | 44 | options = { version: 'version 2.0' }; 45 | parsed = opentype.parse(svg2ttf(fixture, options).buffer.buffer); 46 | assert.strictEqual(parsed.tables.name.version.en, 'Version 2.0'); 47 | }); 48 | }); 49 | 50 | 51 | describe('glyphs', function () { 52 | it('should return 3 glyphs', function () { 53 | let parsed = opentype.parse(svg2ttf(fixture).buffer.buffer); 54 | 55 | assert.strictEqual(parsed.glyphs.length, 3); 56 | assert.strictEqual(parsed.glyphs.glyphs[0].name, ''); // missing-glyph 57 | assert.strictEqual(parsed.glyphs.glyphs[1].name, 'duckduckgo'); 58 | assert.strictEqual(parsed.glyphs.glyphs[2].name, 'github'); 59 | }); 60 | }); 61 | 62 | 63 | describe('os/2 table', function () { 64 | let parsed = opentype.parse(svg2ttf(fixture).buffer.buffer); 65 | let os2 = parsed.tables.os2; 66 | 67 | it('winAscent + winDescent should include line gap', function () { 68 | // https://www.high-logic.com/font-editor/fontcreator/tutorials/font-metrics-vertical-line-spacing 69 | 70 | // always should be >=, but for this specific test they should be equal 71 | assert.strictEqual( 72 | os2.usWinAscent + os2.usWinDescent, 73 | os2.sTypoAscender - os2.sTypoDescender + os2.sTypoLineGap); 74 | }); 75 | 76 | it('os2.version = 4', function () { 77 | assert.strictEqual(os2.version, 4); 78 | }); 79 | }); 80 | 81 | 82 | it('should return an error if glyph has too large bounding box', function () { 83 | assert.throws(() => svg2ttf(` 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | `), /xMax value .* is out of bounds/); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /ttfinfo.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Internal utility qu quickly check ttf tables size 5 | */ 6 | 7 | /*eslint-disable no-console*/ 8 | 9 | 'use strict'; 10 | 11 | 12 | var fs = require('fs'); 13 | var _ = require('lodash'); 14 | var format = require('util').format; 15 | 16 | var ArgumentParser = require('argparse').ArgumentParser; 17 | 18 | var parser = new ArgumentParser({ 19 | add_help: true, 20 | description: 'Dump TTF tables info' 21 | }); 22 | 23 | parser.add_argument('infile', { 24 | nargs: 1, 25 | help: 'Input file' 26 | }); 27 | 28 | parser.add_argument('-d', '--details', { 29 | help: 'Show table dump', 30 | action: 'store_true', 31 | required: false 32 | }); 33 | 34 | var args = parser.parse_args(); 35 | var ttf; 36 | 37 | try { 38 | ttf = fs.readFileSync(args.infile[0]); 39 | } catch (e) { 40 | console.error("Can't open input file (%s)", args.infile[0]); 41 | process.exit(1); 42 | } 43 | 44 | var tablesCount = ttf.readUInt16BE(4); 45 | 46 | var i, offset, headers = []; 47 | 48 | for (i = 0; i < tablesCount; i++) { 49 | offset = 12 + i * 16; 50 | headers.push({ 51 | name: String.fromCharCode( 52 | ttf.readUInt8(offset), 53 | ttf.readUInt8(offset + 1), 54 | ttf.readUInt8(offset + 2), 55 | ttf.readUInt8(offset + 3) 56 | ), 57 | offset: ttf.readUInt32BE(offset + 8), 58 | length: ttf.readUInt32BE(offset + 12) 59 | }); 60 | } 61 | 62 | console.log(format('Tables count: %d'), tablesCount); 63 | 64 | _.forEach(_.sortBy(headers, 'offset'), function (info) { 65 | console.log('- %s: %d bytes (%d offset)', info.name, info.length, info.offset); 66 | if (args.details) { 67 | var bufTable = ttf.slice(info.offset, info.offset + info.length); 68 | var count = Math.floor(bufTable.length / 32); 69 | var offset = 0; 70 | 71 | //split buffer to the small chunks to fit the screen 72 | for (var i = 0; i < count; i++) { 73 | console.log(bufTable.slice(offset, offset + 32)); 74 | offset += 32; 75 | } 76 | 77 | //output the rest 78 | if (offset < (info.length)) { 79 | console.log(bufTable.slice(offset, info.length)); 80 | } 81 | 82 | console.log(''); 83 | } 84 | }); 85 | --------------------------------------------------------------------------------