-
Notifications
You must be signed in to change notification settings - Fork 52
/
Copy pathTextFontMetrics.js
273 lines (236 loc) · 9.61 KB
/
TextFontMetrics.js
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import _ from 'lodash'
import {ATTR, hasAttributeFor} from './attributes'
const SUPER_SUB_FONT_RATIO = 0.65 // matches MS word according to http://en.wikipedia.org/wiki/Subscript_and_superscript
function calcFontScale(fontSize, unitsPerEm) {
return 1 / unitsPerEm * fontSize
}
function calcSuperSubFontSize(fontSize, minFontSize) {
let superSubFontSize = Math.round(fontSize * SUPER_SUB_FONT_RATIO)
return superSubFontSize > minFontSize ? superSubFontSize : minFontSize
}
function charFontStyle(char) {
let attrs = char.attributes
if(!attrs) return 'regular'
let bold = false
let italic = false
if(!_.isUndefined(attrs[ATTR.BOLD])) bold = attrs[ATTR.BOLD]
if(!_.isUndefined(attrs[ATTR.ITALIC])) italic = attrs[ATTR.ITALIC]
if(bold && italic) return 'boldItalic'
else if(bold) return 'bold'
else if(italic) return 'italic'
else return 'regular'
}
function calcFontSizeFromAttributes(fontSize, minFontSize, attributes) {
let hasAttribute = hasAttributeFor(attributes)
// superscript and subscript affect the font size
let superscript = hasAttribute(ATTR.SUPERSCRIPT)
let subscript = hasAttribute(ATTR.SUBSCRIPT)
return superscript || subscript ? calcSuperSubFontSize(fontSize, minFontSize) : fontSize
}
function fontSpec(fontSize, font) {
let styleSpec = font.styleName === 'Regular' ? '' : `${font.styleName} `
let fontSizeSpec = `${fontSize}px `
let name = font.familyName
return styleSpec + fontSizeSpec + name
}
function calcCharAdvanceOpenType(char, fontSize, font, unitsPerEm) {
let glyph = font.charToGlyph(char)
return glyph.unicode ?
glyph.advanceWidth * calcFontScale(fontSize, unitsPerEm) :
// font doesn't contain a glyph for this char, fallback to canvas measurement
calcTextAdvanceCanvas(char, fontSize, font)
}
let canvas = _.memoize(function() {
return document.createElement('canvas')
})
let canvasContext = _.memoize(function() {
return canvas().getContext('2d')
})
function clearCanvas() {
let c = canvas()
canvasContext().clearRect(0, 0, c.width, c.height)
}
function calcTextAdvanceCanvas(text, fontSize, font) {
// need to override newline handling, measureText doesn't handle it correctly (returns a non-zero width)
if(text === '\n') {
return 0
}
let context = canvasContext()
context.font = fontSpec(fontSize, font)
return context.measureText(text).width
}
/**
* Tests various string widths and compares the advance width (in pixels) results between OpenType.js and
* the canvas fallback mechanism which returns the browser's actual rendered font width.
* @type {Function}
*/
let isOpenTypeJsReliable = _.memoize(function(fontSize, font, unitsPerEm) {
let strings = [
'111111111111111111111111111111',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'iiiiiiiiiiiiiiiiiiiiiiiiiiiiii',
'wwwwwwwwwwwwwwwwwwwwwwwwwwwwww',
'Lorem ipsum dolor sit amet, libris essent labitur duo cu.'
]
let reduceOt = function(currentAdvance, char) {
return currentAdvance + calcCharAdvanceOpenType(char, fontSize, font, unitsPerEm)
}
let reduceCanvas = function(currentAdvance, char) {
return currentAdvance + calcTextAdvanceCanvas(char, fontSize, font)
}
let reliable = true
for(let candidate of strings) {
let chars = candidate.split('')
let advanceOt = chars.reduce(reduceOt, 0)
let advanceCanvas = calcTextAdvanceCanvas(candidate, fontSize, font)
let delta = Math.abs(advanceOt - advanceCanvas)
if(delta > 1) {
console.warn(`OpenType.js NOT reliable on this browser/OS (or font not loaded):
Candidate = [${candidate}], Fontspec = ${fontSpec(fontSize, font)}, Δ = ${delta}px
Falling back to slower canvas measurement mechanism.`)
// test if canvas char-by-char width additions are the same as canvas total text width
// if this ever returns false, then the current approach will need to be refactored, see docs on calcCharAdvance
let advanceCanvasByChar = chars.reduce(reduceCanvas, 0)
let deltaCanvas = Math.abs(advanceCanvas - advanceCanvasByChar)
if(deltaCanvas > 1) {
console.error(`Canvas char-by-char width != canvas text width, oops!
Candidate = [${candidate}], Fontspec = ${fontSpec(fontSize, font)}, Δ ot = ${delta}px, Δ canvas = ${deltaCanvas}px
Please report this along with your browser/OS details.`)
}
reliable = false
break
}
}
// clear the canvas, not really necessary as measureText shouldn't write anything there
clearCanvas()
return reliable
}, (fontSize, font, unitsPerEm) => fontSpec(fontSize, font) + ' ' + unitsPerEm)
/**
* Calculate the advance in pixels for a given char. In some browsers/platforms/font sizes, the fonts are not
* rendered according to the specs in the font (see
* http://stackoverflow.com/questions/30922573/firefox-rendering-of-opentype-font-does-not-match-the-font-specification).
* Therefore, ensure the font spec matches the actual rendered width (via the canvas `measureText` method), and use
* the font spec if it matches, otherwise fall back to the (slower) measuredText option.
*
* NOTE there may still be one difference between the browser's rendering and canvas-based calculations here: the
* browser renders entire strings within elements, whereas this calculation renders one character to the canvas at
* a time and adds up the widths. These two approaches seem to be equivalent except for IE in compatibility mode.
*
* TODO refactor mixin to deal with chunks of styled text rather than chars for IE in compatibility mode
*/
function calcCharAdvance(char, fontSize, font, unitsPerEm) {
return isOpenTypeJsReliable(fontSize, font, unitsPerEm) ?
calcCharAdvanceOpenType(char, fontSize, font, unitsPerEm) :
calcTextAdvanceCanvas(char, fontSize, font)
}
function calcReplicaCharAdvance(replicaChar, fontSize, fonts, minFontSize, unitsPerEm) {
let style = charFontStyle(replicaChar)
let charFontSize = calcFontSizeFromAttributes(fontSize, minFontSize, replicaChar.attributes)
return calcCharAdvance(replicaChar.char, charFontSize, fonts[style], unitsPerEm)
}
export default {
setConfig(config) {
this.config = config
},
/**
* Get the font scale to convert between font units and pixels for the given font size.
* @param fontSize
* @return {number}
*/
fontScale(fontSize) {
return calcFontScale(fontSize, this.config.unitsPerEm)
},
/**
* Return the font size given the default font size and current attributes.
* @param fontSize
* @param attributes
*/
fontSizeFromAttributes(fontSize, attributes) {
return calcFontSizeFromAttributes(fontSize, this.config.minFontSize, attributes)
},
/**
* Determines the advance for a given replica char.
* @param char The replica char object.
* @param fontSize
* @return {number}
*/
replicaCharAdvance(char, fontSize) {
return calcReplicaCharAdvance(char, fontSize, this.config.fonts, this.config.minFontSize, this.config.unitsPerEm)
},
/**
* Determines the advance for a given char. Since it is not a replica char, the font style and other attribute
* information cannot be determined. A normal weight, non-decorated font with no special attributes is assumed.
*/
charAdvance(char, fontSize, font) {
return calcCharAdvance(char, fontSize, font, this.config.unitsPerEm)
},
/**
* Returns the advance width in pixels for a space character in the normal style.
*/
advanceXForSpace(fontSize) {
return this.charAdvance(' ', fontSize, this.config.fonts.regular)
},
/**
* Obtain an Object with the char id and cursor position for a given pixel value. This is used to
* set the current character and position the cursor correctly on a mouse click. If the target position
* is past the last character, the index of the last character is returned.
* @param {number} fontSize
* @param {number} pixelValue
* @param {Array} chars The characters used to compare against the given pixel value.
* @return {Object} The cursor x position (cursorX) between characters, and the 0-based character index
* (index) for the given x value.
*/
indexAndCursorForXValue(fontSize, pixelValue, chars) {
let minFontSize = this.config.minFontSize
fontSize = fontSize > minFontSize ? fontSize : minFontSize
let currentWidthPx = 0
let index = 0
for(let i = 0; i < chars.length; i++) {
let glyphAdvancePx = this.replicaCharAdvance(chars[i], fontSize)
if(pixelValue < currentWidthPx + glyphAdvancePx / 2) {
return {
cursorX: currentWidthPx,
index: index
}
} else {
currentWidthPx += glyphAdvancePx
if(glyphAdvancePx > 0) index++
}
}
return {
cursorX: currentWidthPx,
index: index
}
},
/**
* Get the advance width in pixels for the given char or chars.
* @param {number} fontSize
* @param {object|Array} chars
* @return {number}
*/
advanceXForChars(fontSize, chars) {
let minFontSize = this.config.minFontSize
fontSize = fontSize > minFontSize ? fontSize : minFontSize
if(_.isArray(chars)) {
return chars.reduce((currentWidthPx, char) => {
return currentWidthPx + this.replicaCharAdvance(char, fontSize)
}, 0)
} else {
return this.replicaCharAdvance(chars, fontSize)
}
},
/**
* Gets the line height in pixels for a given font size, using the bold font.
*/
lineHeight(fontSize) {
let fontHeader = this.config.fonts.bold.tables.head
return (fontHeader.yMax - fontHeader.yMin) * this.fontScale(fontSize)
},
/**
* Gets the height in pixels of the top of the font, relative to the baseline, using the bold font.
*/
top(fontSize) {
let fontHeader = this.config.fonts.bold.tables.head
return fontHeader.yMax * this.fontScale(fontSize)
}
}