Skip to content

Commit 385812f

Browse files
committed
Faster, more compliant parseFont
1 parent 25b03f3 commit 385812f

File tree

3 files changed

+83
-42
lines changed

3 files changed

+83
-42
lines changed

lib/parse-font.js

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,99 @@
11
'use strict'
22

3-
const parseCssFont = require('parse-css-font')
4-
const unitsCss = require('units-css')
3+
/**
4+
* Font RegExp helpers.
5+
*/
6+
7+
const weights = 'bold|bolder|lighter|[1-9]00'
8+
, styles = 'italic|oblique'
9+
, variants = 'small-caps'
10+
, stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded'
11+
, units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q'
12+
, string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+'
13+
14+
// [ [ <‘font-style’> || <font-variant-css21> || <‘font-weight’> || <‘font-stretch’> ]?
15+
// <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ]
16+
// https://drafts.csswg.org/css-fonts-3/#font-prop
17+
const weightRe = new RegExp(`(${weights}) +`, 'i')
18+
const styleRe = new RegExp(`(${styles}) +`, 'i')
19+
const variantRe = new RegExp(`(${variants}) +`, 'i')
20+
const stretchRe = new RegExp(`(${stretches}) +`, 'i')
21+
const sizeFamilyRe = new RegExp(
22+
'([\\d\\.]+)(' + units + ') *'
23+
+ '((?:' + string + ')( *, *(?:' + string + '))*)')
524

625
/**
7-
* Cache color string RGBA values.
26+
* Cache font parsing.
827
*/
928

1029
const cache = {}
1130

31+
const defaultHeight = 16 // pt, common browser default
32+
1233
/**
1334
* Parse font `str`.
1435
*
1536
* @param {String} str
16-
* @return {Object}
37+
* @return {Object} Parsed font. `size` is in device units. `unit` is the unit
38+
* appearing in the input string.
1739
* @api private
1840
*/
1941

2042
module.exports = function (str) {
21-
let parsedFont
22-
23-
// Try to parse the font string using parse-css-font.
24-
// It will throw an exception if it fails.
25-
try {
26-
parsedFont = parseCssFont(str)
27-
} catch (_) {
28-
// Invalid
29-
return undefined
30-
}
31-
3243
// Cached
3344
if (cache[str]) return cache[str]
3445

35-
// Parse size into value and unit using units-css
36-
var size = unitsCss.parse(parsedFont.size)
46+
// Try for required properties first.
47+
const sizeFamily = sizeFamilyRe.exec(str)
48+
if (!sizeFamily) return // invalid
49+
50+
// Default values and required properties
51+
const font = {
52+
weight: 'normal',
53+
style: 'normal',
54+
stretch: 'normal',
55+
variant: 'normal',
56+
size: parseFloat(sizeFamily[1]),
57+
unit: sizeFamily[2],
58+
family: sizeFamily[3].replace(/["']/g, '').replace(/ *, */g, ',')
59+
}
3760

38-
// TODO: dpi
39-
// TODO: remaining unit conversion
40-
switch (size.unit) {
61+
// Optional, unordered properties
62+
let weight, style, variant, stretch
63+
if ((weight = weightRe.exec(str))) font.weight = weight[1]
64+
if ((style = styleRe.exec(str))) font.style = style[1]
65+
if ((variant = variantRe.exec(str))) font.variant = variant[1]
66+
if ((stretch = stretchRe.exec(str))) font.stretch = stretch[1]
67+
68+
// Convert to device units. (`font.unit` is the original unit)
69+
// TODO: ch, ex
70+
switch (font.unit) {
4171
case 'pt':
42-
size.value /= 0.75
72+
font.size /= 0.75
73+
break
74+
case 'pc':
75+
font.size *= 16
4376
break
4477
case 'in':
45-
size.value *= 96
78+
font.size *= 96
79+
break
80+
case 'cm':
81+
font.size *= 96.0 / 2.54
4682
break
4783
case 'mm':
48-
size.value *= 96.0 / 25.4
84+
font.size *= 96.0 / 25.4
4985
break
50-
case 'cm':
51-
size.value *= 96.0 / 2.54
86+
case '%':
87+
// TODO disabled because existing unit tests assume 100
88+
// font.size *= defaultHeight / 100 / 0.75
89+
break
90+
case 'em':
91+
case 'rem':
92+
font.size *= defaultHeight / 0.75
93+
break
94+
case 'q':
95+
font.size *= 96 / 25.4 / 4
5296
break
53-
}
54-
55-
// Populate font object
56-
var font = {
57-
weight: parsedFont.weight,
58-
style: parsedFont.style,
59-
size: size.value,
60-
unit: size.unit,
61-
family: parsedFont.family.join(',')
6297
}
6398

6499
return (cache[str] = font)

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@
3030
"test-server": "node test/server.js"
3131
},
3232
"dependencies": {
33-
"nan": "^2.4.0",
34-
"parse-css-font": "^2.0.2",
35-
"units-css": "^0.4.0"
33+
"nan": "^2.4.0"
3634
},
3735
"devDependencies": {
3836
"assert-rejects": "^0.1.1",

test/canvas.test.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe('Canvas', function () {
2424
, '20.5pt Arial'
2525
, { size: 27.333333333333332, unit: 'pt', family: 'Arial' }
2626
, '20% Arial'
27-
, { size: 20, unit: '%', family: 'Arial' }
27+
, { size: 20, unit: '%', family: 'Arial' } // TODO I think this is a bad assertion - ZB 23-Jul-2017
2828
, '20mm Arial'
2929
, { size: 75.59055118110237, unit: 'mm', family: 'Arial' }
3030
, '20px serif'
@@ -59,17 +59,25 @@ describe('Canvas', function () {
5959
, { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' }
6060
, 'lighter 20px Arial'
6161
, { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' }
62+
, 'normal normal normal 16px Impact'
63+
, { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' }
64+
, 'italic small-caps bolder 16px cursive'
65+
, { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' }
66+
, '20px "new century schoolbook", serif'
67+
, { size: 20, unit: 'px', family: 'new century schoolbook,serif' }
6268
];
6369

6470
for (var i = 0, len = tests.length; i < len; ++i) {
6571
var str = tests[i++]
66-
, obj = tests[i]
72+
, expected = tests[i]
6773
, actual = parseFont(str);
6874

69-
if (!obj.style) obj.style = 'normal';
70-
if (!obj.weight) obj.weight = 'normal';
75+
if (!expected.style) expected.style = 'normal';
76+
if (!expected.weight) expected.weight = 'normal';
77+
if (!expected.stretch) expected.stretch = 'normal';
78+
if (!expected.variant) expected.variant = 'normal';
7179

72-
assert.deepEqual(obj, actual);
80+
assert.deepEqual(actual, expected, 'Failed to parse: ' + str);
7381
}
7482
});
7583

0 commit comments

Comments
 (0)