From 4625efae9661ba0a42fee994cbebc1abe0d2102e Mon Sep 17 00:00:00 2001 From: Zach Bjornson Date: Sun, 23 Jul 2017 21:30:40 -0700 Subject: [PATCH] Faster, more compliant parseFont --- lib/parse-font.js | 105 ++++++++++++++++++++++++++++++-------------- package.json | 4 +- test/canvas.test.js | 20 ++++++--- 3 files changed, 87 insertions(+), 42 deletions(-) diff --git a/lib/parse-font.js b/lib/parse-font.js index b42bca471..cf91b6e61 100644 --- a/lib/parse-font.js +++ b/lib/parse-font.js @@ -1,64 +1,101 @@ 'use strict' -const parseCssFont = require('parse-css-font') -const unitsCss = require('units-css') +/** + * Font RegExp helpers. + */ + +const weights = 'bold|bolder|lighter|[1-9]00' + , styles = 'italic|oblique' + , variants = 'small-caps' + , stretches = 'ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded' + , units = 'px|pt|pc|in|cm|mm|%|em|ex|ch|rem|q' + , string = '\'([^\']+)\'|"([^"]+)"|[\\w-]+' + +// [ [ <‘font-style’> || || <‘font-weight’> || <‘font-stretch’> ]? +// <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] +// https://drafts.csswg.org/css-fonts-3/#font-prop +const weightRe = new RegExp(`(${weights}) +`, 'i') +const styleRe = new RegExp(`(${styles}) +`, 'i') +const variantRe = new RegExp(`(${variants}) +`, 'i') +const stretchRe = new RegExp(`(${stretches}) +`, 'i') +const sizeFamilyRe = new RegExp( + '([\\d\\.]+)(' + units + ') *' + + '((?:' + string + ')( *, *(?:' + string + '))*)') /** - * Cache color string RGBA values. + * Cache font parsing. */ const cache = {} +const defaultHeight = 16 // pt, common browser default + /** * Parse font `str`. * * @param {String} str - * @return {Object} + * @return {Object} Parsed font. `size` is in device units. `unit` is the unit + * appearing in the input string. * @api private */ module.exports = function (str) { - let parsedFont - - // Try to parse the font string using parse-css-font. - // It will throw an exception if it fails. - try { - parsedFont = parseCssFont(str) - } catch (_) { - // Invalid - return undefined - } - // Cached if (cache[str]) return cache[str] - // Parse size into value and unit using units-css - var size = unitsCss.parse(parsedFont.size) + // Try for required properties first. + const sizeFamily = sizeFamilyRe.exec(str) + if (!sizeFamily) return // invalid + + // Default values and required properties + const font = { + weight: 'normal', + style: 'normal', + stretch: 'normal', + variant: 'normal', + size: parseFloat(sizeFamily[1]), + unit: sizeFamily[2], + family: sizeFamily[3].replace(/["']/g, '').replace(/ *, */g, ',') + } - // TODO: dpi - // TODO: remaining unit conversion - switch (size.unit) { + // Optional, unordered properties. + let weight, style, variant, stretch + // Stop search at `sizeFamily.index` + let substr = str.substring(0, sizeFamily.index) + if ((weight = weightRe.exec(substr))) font.weight = weight[1] + if ((style = styleRe.exec(substr))) font.style = style[1] + if ((variant = variantRe.exec(substr))) font.variant = variant[1] + if ((stretch = stretchRe.exec(substr))) font.stretch = stretch[1] + + // Convert to device units. (`font.unit` is the original unit) + // TODO: ch, ex + switch (font.unit) { case 'pt': - size.value /= 0.75 + font.size /= 0.75 + break + case 'pc': + font.size *= 16 break case 'in': - size.value *= 96 + font.size *= 96 + break + case 'cm': + font.size *= 96.0 / 2.54 break case 'mm': - size.value *= 96.0 / 25.4 + font.size *= 96.0 / 25.4 break - case 'cm': - size.value *= 96.0 / 2.54 + case '%': + // TODO disabled because existing unit tests assume 100 + // font.size *= defaultHeight / 100 / 0.75 + break + case 'em': + case 'rem': + font.size *= defaultHeight / 0.75 + break + case 'q': + font.size *= 96 / 25.4 / 4 break - } - - // Populate font object - var font = { - weight: parsedFont.weight, - style: parsedFont.style, - size: size.value, - unit: size.unit, - family: parsedFont.family.join(',') } return (cache[str] = font) diff --git a/package.json b/package.json index dc620ac24..ade5f0d3c 100644 --- a/package.json +++ b/package.json @@ -30,9 +30,7 @@ "test-server": "node test/server.js" }, "dependencies": { - "nan": "^2.4.0", - "parse-css-font": "^2.0.2", - "units-css": "^0.4.0" + "nan": "^2.4.0" }, "devDependencies": { "assert-rejects": "^0.1.1", diff --git a/test/canvas.test.js b/test/canvas.test.js index a307f51af..d0e7efd17 100644 --- a/test/canvas.test.js +++ b/test/canvas.test.js @@ -24,7 +24,7 @@ describe('Canvas', function () { , '20.5pt Arial' , { size: 27.333333333333332, unit: 'pt', family: 'Arial' } , '20% Arial' - , { size: 20, unit: '%', family: 'Arial' } + , { size: 20, unit: '%', family: 'Arial' } // TODO I think this is a bad assertion - ZB 23-Jul-2017 , '20mm Arial' , { size: 75.59055118110237, unit: 'mm', family: 'Arial' } , '20px serif' @@ -59,17 +59,27 @@ describe('Canvas', function () { , { size: 20, unit: 'px', weight: 'bolder', family: 'Arial' } , 'lighter 20px Arial' , { size: 20, unit: 'px', weight: 'lighter', family: 'Arial' } + , 'normal normal normal 16px Impact' + , { size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' } + , 'italic small-caps bolder 16px cursive' + , { size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' } + , '20px "new century schoolbook", serif' + , { size: 20, unit: 'px', family: 'new century schoolbook,serif' } + , '20px "Arial bold 300"' // synthetic case with weight keyword inside family + , { size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' } ]; for (var i = 0, len = tests.length; i < len; ++i) { var str = tests[i++] - , obj = tests[i] + , expected = tests[i] , actual = parseFont(str); - if (!obj.style) obj.style = 'normal'; - if (!obj.weight) obj.weight = 'normal'; + if (!expected.style) expected.style = 'normal'; + if (!expected.weight) expected.weight = 'normal'; + if (!expected.stretch) expected.stretch = 'normal'; + if (!expected.variant) expected.variant = 'normal'; - assert.deepEqual(obj, actual); + assert.deepEqual(actual, expected, 'Failed to parse: ' + str); } });