├── .npmignore ├── test.js ├── .gitignore ├── package.json ├── .eslintrc.json ├── readme.md ├── index.js └── sphinx.svg /.npmignore: -------------------------------------------------------------------------------- 1 | sphinx.svg 2 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | let m = require('./') 4 | let a = require('assert') 5 | 6 | 7 | let fix = { 8 | top:0, 9 | bottom:1.125, 10 | lineHeight:1.125, 11 | alphabetic:0.890625, 12 | baseline:0.890625, 13 | middle:0.546875, 14 | median:0.546875, 15 | hanging:0.171875, 16 | ideographic:1.109375, 17 | upper:0.1875, 18 | capHeight:0.703125, 19 | lower:0.375, 20 | xHeight:0.515625, 21 | tittle:0.1875, 22 | ascent:0.1875, 23 | descent:1.09375, 24 | overshoot:0.015625 25 | } 26 | 27 | let res = m('sans-serif', {fontSize: 64}) 28 | a.deepEqual(m('sans-serif', {fontSize: 64}), fix) 29 | a.deepEqual(m('sans-serif', {fontSize: 64, fontWeight: '700', fontStyle: 'italic'}), fix) 30 | a.equal(m('sans-serif', {fontSize: 64, origin: 'baseline'}).baseline, 0) 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | 3 | node_modules 4 | # Compiled source # 5 | ################### 6 | *.com 7 | *.class 8 | *.dll 9 | *.exe 10 | *.o 11 | *.so 12 | *.bin 13 | 14 | # Packages # 15 | ############ 16 | # it's better to unpack these files and commit the raw source 17 | # git has its own built in compression methods 18 | *.7z 19 | *.dmg 20 | *.gz 21 | *.iso 22 | *.jar 23 | *.rar 24 | *.tar 25 | *.zip 26 | 27 | # Logs and databases # 28 | ###################### 29 | *.log 30 | *.sql 31 | *.sqlite 32 | 33 | # OS generated files # 34 | ###################### 35 | .DS_Store 36 | .DS_Store? 37 | *._* 38 | .Spotlight-V100 39 | .Trashes 40 | Icon? 41 | ehthumbs.db 42 | Thumbs.db 43 | 44 | # My extension # 45 | ################ 46 | *.lock 47 | *.bak 48 | lsn 49 | *.dump 50 | *.beam 51 | *.[0-9] 52 | *._[0-9] 53 | *.ns 54 | Scripting_* 55 | docs 56 | *.pdf 57 | *.pak 58 | 59 | 60 | design 61 | instances 62 | *node_modules 63 | demo 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "font-measure", 3 | "version": "1.2.2", 4 | "description": "Calculate metrics of a font", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "budo test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/dy/font-measure.git" 12 | }, 13 | "keywords": [ 14 | "font", 15 | "type", 16 | "typography", 17 | "metrics", 18 | "measure", 19 | "line-height", 20 | "vertical", 21 | "vertical-rhythm", 22 | "baseline", 23 | "ascender", 24 | "descender", 25 | "alphabetic", 26 | "text-baseline", 27 | "alphabetic", 28 | "hanging", 29 | "overshoot" 30 | ], 31 | "author": "Dmitry Yv ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/dy/font-measure/issues" 35 | }, 36 | "homepage": "https://github.com/dy/font-measure#readme", 37 | "dependencies": { 38 | "css-font": "^1.2.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "strict": 2, 11 | "indent": 0, 12 | "linebreak-style": 0, 13 | "quotes": 0, 14 | "semi": 0, 15 | "no-cond-assign": 1, 16 | "no-constant-condition": 1, 17 | "no-duplicate-case": 1, 18 | "no-empty": 1, 19 | "no-ex-assign": 1, 20 | "no-extra-boolean-cast": 1, 21 | "no-extra-semi": 1, 22 | "no-fallthrough": 1, 23 | "no-func-assign": 1, 24 | "no-global-assign": 1, 25 | "no-implicit-globals": 2, 26 | "no-inner-declarations": ["error", "functions"], 27 | "no-irregular-whitespace": 2, 28 | "no-loop-func": 1, 29 | "no-magic-numbers": ["warn", { "ignore": [1, 0, -1], "ignoreArrayIndexes": true}], 30 | "no-multi-str": 1, 31 | "no-mixed-spaces-and-tabs": 1, 32 | "no-proto": 1, 33 | "no-sequences": 1, 34 | "no-throw-literal": 1, 35 | "no-unmodified-loop-condition": 1, 36 | "no-useless-call": 1, 37 | "no-void": 1, 38 | "no-with": 2, 39 | "wrap-iife": 1, 40 | "no-redeclare": 1, 41 | "no-unused-vars": ["error", { "vars": "all", "args": "none" }], 42 | "no-sparse-arrays": 1 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # font-measure [![unstable](https://img.shields.io/badge/stability-unstable-green.svg)](http://github.com/badges/stability-badges) 2 | 3 | Calculate metrics for a font. 4 | 5 | [![npm install font-measure](https://nodei.co/npm/font-measure.png?mini=true)](https://npmjs.org/package/font-measure/) 6 | 7 | ```js 8 | let measure = requrie('font-measure') 9 | 10 | measure('Roboto') 11 | 12 | /* 13 | { 14 | top: 0, 15 | median: 0.640625, 16 | middle: 0.640625, 17 | bottom: 1.3125, 18 | alphabetic: 1.03125, 19 | baseline: 1.03125, 20 | upper: 0.328125, 21 | lower: 0.515625, 22 | capHeight: 0.703125, 23 | xHeight: 0.515625 24 | ascent: 0.28125, 25 | descent: 1.234375, 26 | hanging: 0.203125, 27 | ideographic: 1.296875, 28 | lineHeight: 1.3125, 29 | overshoot: 0.015625, 30 | tittle: 0.28125, 31 | } 32 | */ 33 | 34 | ``` 35 | 36 | ## API 37 | 38 | ### `let metrics = measure(family, options?)` 39 | 40 | Get metrics data for a font family or [CSS font string](), possibly with custom options. Font can be a string or an array with fonts. 41 | 42 | #### `metrics`: 43 | 44 | 45 | 46 | 47 | #### `options`: 48 | 49 | Property | Default | Meaning 50 | ---|---|--- 51 | `origin` | `top` | Origin for metrics. Can be changed to `baseline` or any other metric. 52 | `fontSize` | `64` | Font-size to use for calculations. Larger size gives higher precision with slower performance. 53 | `fontWeight` | `normal` | Font weight to use for calculations, eg. `bold`, `700` etc. 54 | `fontStyle` | `normal` | Font style to use for calculations, eg. `italic`, `oblique`. 55 | `canvas` | `measure.canvas` | Canvas to use for measurements. 56 | `tittle` | `i` | Character to detect tittle. `null` disables calculation. 57 | `descent` | `p` | Character to detect descent line. `null` disables calculation. 58 | `ascent` | `h` | Character to detect ascent line. `null` disables calculation. 59 | `overshoot` | `O` | Character to detect overshoot. `null` disables calculation. 60 | `upper` | `H` | Character to detect upper line / cap-height. `null` disables calculation. 61 | `lower` | `x` | Character to detect lower line / x-height. `null` disables calculation. 62 | 63 | 64 | ## See also 65 | 66 | * [optical-properties](https://ghub.io/optical-properties) − calculate image/character optical center and bounding box. 67 | * [detect-kerning](https://ghub.io/detect-kerning) − calculate kerning pairs for a font. 68 | 69 | ## Related 70 | 71 | There are many text / font measuring packages for the moment, but most of them don't satisfy basic quality requirements. Special thanks to @soulwire 72 | for [fontmetrics](https://ghub.io/fontmetrics) as model implementation. 73 | 74 | ## License 75 | 76 | © 2018 Dima Yv. MIT License 77 | 78 | Development supported by plot.ly. 79 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = measure 4 | 5 | measure.canvas = document.createElement('canvas') 6 | measure.cache = {} 7 | 8 | function measure (font, o) { 9 | if (!o) o = {} 10 | 11 | if (typeof font === 'string' || Array.isArray(font)) { 12 | o.family = font 13 | } 14 | 15 | var family = Array.isArray(o.family) ? o.family.join(', ') : o.family 16 | if (!family) throw Error('`family` must be defined') 17 | 18 | var fs = o.size || o.fontSize || o.em || 48 19 | var weight = o.weight || o.fontWeight || '' 20 | var style = o.style || o.fontStyle || '' 21 | var font = [style, weight, fs].join(' ') + 'px ' + family 22 | var origin = o.origin || 'top' 23 | 24 | if (measure.cache[family]) { 25 | // return more precise values if cache has them 26 | if (fs <= measure.cache[family].em) { 27 | return applyOrigin(measure.cache[family], origin) 28 | } 29 | } 30 | 31 | var canvas = o.canvas || measure.canvas 32 | var ctx = canvas.getContext('2d') 33 | var chars = { 34 | upper: o.upper !== undefined ? o.upper : 'H', 35 | lower: o.lower !== undefined ? o.lower : 'x', 36 | descent: o.descent !== undefined ? o.descent : 'p', 37 | ascent: o.ascent !== undefined ? o.ascent : 'h', 38 | tittle: o.tittle !== undefined ? o.tittle : 'i', 39 | overshoot: o.overshoot !== undefined ? o.overshoot : 'O' 40 | } 41 | var l = Math.ceil(fs * 1.5) 42 | canvas.height = l 43 | canvas.width = l * .5 44 | ctx.font = font 45 | 46 | var char = 'H' 47 | var result = { 48 | top: 0 49 | } 50 | 51 | // measure line-height 52 | ctx.clearRect(0, 0, l, l) 53 | ctx.textBaseline = 'top' 54 | ctx.fillStyle = 'black' 55 | ctx.fillText(char, 0, 0) 56 | var topPx = firstTop(ctx.getImageData(0, 0, l, l)) 57 | ctx.clearRect(0, 0, l, l) 58 | ctx.textBaseline = 'bottom' 59 | ctx.fillText(char, 0, l) 60 | var bottomPx = firstTop(ctx.getImageData(0, 0, l, l)) 61 | result.lineHeight = 62 | result.bottom = l - bottomPx + topPx 63 | 64 | // measure baseline 65 | ctx.clearRect(0, 0, l, l) 66 | ctx.textBaseline = 'alphabetic' 67 | ctx.fillText(char, 0, l) 68 | var baselinePx = firstTop(ctx.getImageData(0, 0, l, l)) 69 | var baseline = l - baselinePx - 1 + topPx 70 | result.baseline = 71 | result.alphabetic = baseline 72 | 73 | // measure median 74 | ctx.clearRect(0, 0, l, l) 75 | ctx.textBaseline = 'middle' 76 | ctx.fillText(char, 0, l * .5) 77 | var medianPx = firstTop(ctx.getImageData(0, 0, l, l)) 78 | result.median = 79 | result.middle = l - medianPx - 1 + topPx - l * .5 80 | 81 | // measure hanging 82 | ctx.clearRect(0, 0, l, l) 83 | ctx.textBaseline = 'hanging' 84 | ctx.fillText(char, 0, l * .5) 85 | var hangingPx = firstTop(ctx.getImageData(0, 0, l, l)) 86 | result.hanging = l - hangingPx - 1 + topPx - l * .5 87 | 88 | // measure ideographic 89 | ctx.clearRect(0, 0, l, l) 90 | ctx.textBaseline = 'ideographic' 91 | ctx.fillText(char, 0, l) 92 | var ideographicPx = firstTop(ctx.getImageData(0, 0, l, l)) 93 | result.ideographic = l - ideographicPx - 1 + topPx 94 | 95 | // measure cap 96 | if (chars.upper) { 97 | ctx.clearRect(0, 0, l, l) 98 | ctx.textBaseline = 'top' 99 | ctx.fillText(chars.upper, 0, 0) 100 | result.upper = firstTop(ctx.getImageData(0, 0, l, l)) 101 | result.capHeight = (result.baseline - result.upper) 102 | } 103 | 104 | // measure x 105 | if (chars.lower) { 106 | ctx.clearRect(0, 0, l, l) 107 | ctx.textBaseline = 'top' 108 | ctx.fillText(chars.lower, 0, 0) 109 | result.lower = firstTop(ctx.getImageData(0, 0, l, l)) 110 | result.xHeight = (result.baseline - result.lower) 111 | } 112 | 113 | // measure tittle 114 | if (chars.tittle) { 115 | ctx.clearRect(0, 0, l, l) 116 | ctx.textBaseline = 'top' 117 | ctx.fillText(chars.tittle, 0, 0) 118 | result.tittle = firstTop(ctx.getImageData(0, 0, l, l)) 119 | } 120 | 121 | // measure ascent 122 | if (chars.ascent) { 123 | ctx.clearRect(0, 0, l, l) 124 | ctx.textBaseline = 'top' 125 | ctx.fillText(chars.ascent, 0, 0) 126 | result.ascent = firstTop(ctx.getImageData(0, 0, l, l)) 127 | } 128 | 129 | // measure descent 130 | if (chars.descent) { 131 | ctx.clearRect(0, 0, l, l) 132 | ctx.textBaseline = 'top' 133 | ctx.fillText(chars.descent, 0, 0) 134 | result.descent = firstBottom(ctx.getImageData(0, 0, l, l)) 135 | } 136 | 137 | // measure overshoot 138 | if (chars.overshoot) { 139 | ctx.clearRect(0, 0, l, l) 140 | ctx.textBaseline = 'top' 141 | ctx.fillText(chars.overshoot, 0, 0) 142 | var overshootPx = firstBottom(ctx.getImageData(0, 0, l, l)) 143 | result.overshoot = overshootPx - baseline 144 | } 145 | 146 | // normalize result 147 | for (var name in result) { 148 | result[name] /= fs 149 | } 150 | 151 | result.em = fs 152 | measure.cache[family] = result 153 | 154 | return applyOrigin(result, origin) 155 | } 156 | 157 | function applyOrigin(obj, origin) { 158 | var res = {} 159 | if (typeof origin === 'string') origin = obj[origin] 160 | for (var name in obj) { 161 | if (name === 'em') continue 162 | res[name] = obj[name] - origin 163 | } 164 | return res 165 | } 166 | 167 | function firstTop(iData) { 168 | var l = iData.height 169 | var data = iData.data 170 | for (var i = 3; i < data.length; i+=4) { 171 | if (data[i] !== 0) { 172 | return Math.floor((i - 3) *.25 / l) 173 | } 174 | } 175 | } 176 | 177 | function firstBottom(iData) { 178 | var l = iData.height 179 | var data = iData.data 180 | for (var i = data.length - 1; i > 0; i -= 4) { 181 | if (data[i] !== 0) { 182 | return Math.floor((i - 3) *.25 / l) 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /sphinx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | --------------------------------------------------------------------------------