Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e5cd5a4

Browse files
committedDec 29, 2024·
add C++ parser for the font shorthand
This is the first part needed for the new font stack, which will look like the FontFace-based API the browsers have. FontFace uses a parser for font-family, font-size, etc., so I will need deeper control over the parser. FontFace will be implemented in C++ and I didn't want to carry over the awkward (and slow) switching between JS and C++. So here it is. I used Claude to generate initial classes and busy work, but it's been heavily examined and heavily modified. Caching aside, this is 3x faster in the benchmarks, but in some cases was 400x faster, which IIRC is why we cached it. I expect the C++ version to have a much more stable performance profile, so not going to cache for now. It's also far more correct than what we had!
1 parent 3a4cc4d commit e5cd5a4

12 files changed

+1130
-210
lines changed
 

‎CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
88
(Unreleased)
99
==================
1010
### Changed
11+
* `ctx.font` has a new C++ parser and is 2x-400x faster. Please file an issue if you experience different results, as caching has been removed.
12+
1113
### Added
1214
### Fixed
1315

‎binding.gyp

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
'src/Image.cc',
7676
'src/ImageData.cc',
7777
'src/init.cc',
78-
'src/register_font.cc'
78+
'src/register_font.cc',
79+
'src/FontParser.cc'
7980
],
8081
'conditions': [
8182
['OS=="win"', {

‎index.js

-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ const Canvas = require('./lib/canvas')
22
const Image = require('./lib/image')
33
const CanvasRenderingContext2D = require('./lib/context2d')
44
const CanvasPattern = require('./lib/pattern')
5-
const parseFont = require('./lib/parse-font')
65
const packageJson = require('./package.json')
76
const bindings = require('./lib/bindings')
87
const fs = require('fs')
@@ -12,7 +11,6 @@ const JPEGStream = require('./lib/jpegstream')
1211
const { DOMPoint, DOMMatrix } = require('./lib/DOMMatrix')
1312

1413
bindings.setDOMMatrix(DOMMatrix)
15-
bindings.setParseFont(parseFont)
1614

1715
function createCanvas (width, height, type) {
1816
return new Canvas(width, height, type)
@@ -73,7 +71,6 @@ exports.DOMPoint = DOMPoint
7371

7472
exports.registerFont = registerFont
7573
exports.deregisterAllFonts = deregisterAllFonts
76-
exports.parseFont = parseFont
7774

7875
exports.createCanvas = createCanvas
7976
exports.createImageData = createImageData

‎lib/parse-font.js

-110
This file was deleted.

‎src/Canvas.cc

+38-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "Util.h"
2222
#include <vector>
2323
#include "node_buffer.h"
24+
#include "FontParser.h"
2425

2526
#ifdef HAVE_JPEG
2627
#include "JPEGStream.h"
@@ -68,7 +69,8 @@ Canvas::Initialize(Napi::Env& env, Napi::Object& exports) {
6869
StaticValue("PNG_FILTER_PAETH", Napi::Number::New(env, PNG_FILTER_PAETH), napi_default_jsproperty),
6970
StaticValue("PNG_ALL_FILTERS", Napi::Number::New(env, PNG_ALL_FILTERS), napi_default_jsproperty),
7071
StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method),
71-
StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method)
72+
StaticMethod<&Canvas::DeregisterAllFonts>("_deregisterAllFonts", napi_default_method),
73+
StaticMethod<&Canvas::ParseFont>("parseFont", napi_default_method)
7274
});
7375

7476
data->CanvasCtor = Napi::Persistent(ctor);
@@ -694,6 +696,7 @@ Canvas::RegisterFont(const Napi::CallbackInfo& info) {
694696
// now check the attrs, there are many ways to be wrong
695697
Napi::Object js_user_desc = info[1].As<Napi::Object>();
696698

699+
// TODO: use FontParser on these values just like the FontFace API works
697700
char *family = str_value(js_user_desc.Get("family"), NULL, false);
698701
char *weight = str_value(js_user_desc.Get("weight"), "normal", true);
699702
char *style = str_value(js_user_desc.Get("style"), "normal", false);
@@ -749,6 +752,40 @@ Canvas::DeregisterAllFonts(const Napi::CallbackInfo& info) {
749752
if (!success) Napi::Error::New(env, "Could not deregister one or more fonts").ThrowAsJavaScriptException();
750753
}
751754

755+
/*
756+
* Do not use! This is only exported for testing
757+
*/
758+
Napi::Value
759+
Canvas::ParseFont(const Napi::CallbackInfo& info) {
760+
Napi::Env env = info.Env();
761+
762+
if (info.Length() != 1) return env.Undefined();
763+
764+
Napi::String str;
765+
if (!info[0].ToString().UnwrapTo(&str)) return env.Undefined();
766+
767+
bool ok;
768+
auto props = FontParser::parse(str, &ok);
769+
if (!ok) return env.Undefined();
770+
771+
Napi::Object obj = Napi::Object::New(env);
772+
obj.Set("size", Napi::Number::New(env, props.fontSize));
773+
Napi::Array families = Napi::Array::New(env);
774+
obj.Set("families", families);
775+
776+
unsigned int index = 0;
777+
778+
for (auto& family : props.fontFamily) {
779+
families[index++] = Napi::String::New(env, family);
780+
}
781+
782+
obj.Set("weight", Napi::Number::New(env, props.fontWeight));
783+
obj.Set("variant", Napi::Number::New(env, static_cast<int>(props.fontVariant)));
784+
obj.Set("style", Napi::Number::New(env, static_cast<int>(props.fontStyle)));
785+
786+
return obj;
787+
}
788+
752789
/*
753790
* Get a PangoStyle from a CSS string (like "italic")
754791
*/

‎src/Canvas.h

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class Canvas : public Napi::ObjectWrap<Canvas> {
6868
void StreamJPEGSync(const Napi::CallbackInfo& info);
6969
static void RegisterFont(const Napi::CallbackInfo& info);
7070
static void DeregisterAllFonts(const Napi::CallbackInfo& info);
71+
static Napi::Value ParseFont(const Napi::CallbackInfo& info);
7172
Napi::Error CairoError(cairo_status_t status);
7273
static void ToPngBufferAsync(Closure* closure);
7374
static void ToJpegBufferAsync(Closure* closure);

‎src/CanvasRenderingContext2d.cc

+18-22
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "CanvasGradient.h"
1010
#include "CanvasPattern.h"
1111
#include "InstanceData.h"
12+
#include "FontParser.h"
1213
#include <cmath>
1314
#include <cstdlib>
1415
#include "Image.h"
@@ -2575,34 +2576,29 @@ Context2d::GetFont(const Napi::CallbackInfo& info) {
25752576

25762577
void
25772578
Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) {
2578-
InstanceData* data = env.GetInstanceData<InstanceData>();
2579-
25802579
if (!value.IsString()) return;
25812580

2582-
if (!value.As<Napi::String>().Utf8Value().length()) return;
2583-
2584-
Napi::Value mparsed;
2581+
std::string str = value.As<Napi::String>().Utf8Value();
2582+
if (!str.length()) return;
25852583

2586-
// parseFont returns undefined for invalid CSS font strings
2587-
if (!data->parseFont.Call({ value }).UnwrapTo(&mparsed) || mparsed.IsUndefined()) return;
2588-
2589-
Napi::Object font = mparsed.As<Napi::Object>();
2590-
2591-
Napi::String empty = Napi::String::New(env, "");
2592-
Napi::Number zero = Napi::Number::New(env, 0);
2593-
2594-
std::string weight = font.Get("weight").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value();
2595-
std::string style = font.Get("style").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value();
2596-
double size = font.Get("size").UnwrapOr(zero).ToNumber().UnwrapOr(zero).DoubleValue();
2597-
std::string unit = font.Get("unit").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value();
2598-
std::string family = font.Get("family").UnwrapOr(empty).ToString().UnwrapOr(empty).Utf8Value();
2584+
bool success;
2585+
auto props = FontParser::parse(str, &success);
2586+
if (!success) return;
25992587

26002588
PangoFontDescription *desc = pango_font_description_copy(state->fontDescription);
26012589
pango_font_description_free(state->fontDescription);
26022590

2603-
pango_font_description_set_style(desc, Canvas::GetStyleFromCSSString(style.c_str()));
2604-
pango_font_description_set_weight(desc, Canvas::GetWeightFromCSSString(weight.c_str()));
2591+
PangoStyle style = props.fontStyle == FontStyle::Italic ? PANGO_STYLE_ITALIC
2592+
: props.fontStyle == FontStyle::Oblique ? PANGO_STYLE_OBLIQUE
2593+
: PANGO_STYLE_NORMAL;
2594+
pango_font_description_set_style(desc, style);
26052595

2596+
pango_font_description_set_weight(desc, static_cast<PangoWeight>(props.fontWeight));
2597+
2598+
std::string family = props.fontFamily.empty() ? "" : props.fontFamily[0];
2599+
for (size_t i = 1; i < props.fontFamily.size(); i++) {
2600+
family += "," + props.fontFamily[i];
2601+
}
26062602
if (family.length() > 0) {
26072603
// See #1643 - Pango understands "sans" whereas CSS uses "sans-serif"
26082604
std::string s1(family);
@@ -2617,12 +2613,12 @@ Context2d::SetFont(const Napi::CallbackInfo& info, const Napi::Value& value) {
26172613
PangoFontDescription *sys_desc = Canvas::ResolveFontDescription(desc);
26182614
pango_font_description_free(desc);
26192615

2620-
if (size > 0) pango_font_description_set_absolute_size(sys_desc, size * PANGO_SCALE);
2616+
if (props.fontSize > 0) pango_font_description_set_absolute_size(sys_desc, props.fontSize * PANGO_SCALE);
26212617

26222618
state->fontDescription = sys_desc;
26232619
pango_layout_set_font_description(_layout, sys_desc);
26242620

2625-
state->font = value.As<Napi::String>().Utf8Value().c_str();
2621+
state->font = str;
26262622
}
26272623

26282624
/*

‎src/CharData.h

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// This is used for classifying characters according to the definition of tokens
2+
// in the CSS standards, but could be extended for any other future uses
3+
4+
#pragma once
5+
6+
namespace CharData {
7+
static constexpr uint8_t Whitespace = 0x1;
8+
static constexpr uint8_t Newline = 0x2;
9+
static constexpr uint8_t Hex = 0x4;
10+
static constexpr uint8_t Nmstart = 0x8;
11+
static constexpr uint8_t Nmchar = 0x10;
12+
static constexpr uint8_t Sign = 0x20;
13+
static constexpr uint8_t Digit = 0x40;
14+
static constexpr uint8_t NumStart = 0x80;
15+
};
16+
17+
using namespace CharData;
18+
19+
constexpr const uint8_t charData[256] = {
20+
0, 0, 0, 0, 0, 0, 0, 0, 0, // 0-8
21+
Whitespace, // 9 (HT)
22+
Whitespace | Newline, // 10 (LF)
23+
0, // 11 (VT)
24+
Whitespace | Newline, // 12 (FF)
25+
Whitespace | Newline, // 13 (CR)
26+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14-31
27+
Whitespace, // 32 (Space)
28+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 33-42
29+
Sign | NumStart, // 43 (+)
30+
0, // 44
31+
Nmchar | Sign | NumStart, // 45 (-)
32+
0, 0, // 46-47
33+
Nmchar | Digit | NumStart | Hex, // 48 (0)
34+
Nmchar | Digit | NumStart | Hex, // 49 (1)
35+
Nmchar | Digit | NumStart | Hex, // 50 (2)
36+
Nmchar | Digit | NumStart | Hex, // 51 (3)
37+
Nmchar | Digit | NumStart | Hex, // 52 (4)
38+
Nmchar | Digit | NumStart | Hex, // 53 (5)
39+
Nmchar | Digit | NumStart | Hex, // 54 (6)
40+
Nmchar | Digit | NumStart | Hex, // 55 (7)
41+
Nmchar | Digit | NumStart | Hex, // 56 (8)
42+
Nmchar | Digit | NumStart | Hex, // 57 (9)
43+
0, 0, 0, 0, 0, 0, 0, // 58-64
44+
Nmstart | Nmchar | Hex, // 65 (A)
45+
Nmstart | Nmchar | Hex, // 66 (B)
46+
Nmstart | Nmchar | Hex, // 67 (C)
47+
Nmstart | Nmchar | Hex, // 68 (D)
48+
Nmstart | Nmchar | Hex, // 69 (E)
49+
Nmstart | Nmchar | Hex, // 70 (F)
50+
Nmstart | Nmchar, // 71 (G)
51+
Nmstart | Nmchar, // 72 (H)
52+
Nmstart | Nmchar, // 73 (I)
53+
Nmstart | Nmchar, // 74 (J)
54+
Nmstart | Nmchar, // 75 (K)
55+
Nmstart | Nmchar, // 76 (L)
56+
Nmstart | Nmchar, // 77 (M)
57+
Nmstart | Nmchar, // 78 (N)
58+
Nmstart | Nmchar, // 79 (O)
59+
Nmstart | Nmchar, // 80 (P)
60+
Nmstart | Nmchar, // 81 (Q)
61+
Nmstart | Nmchar, // 82 (R)
62+
Nmstart | Nmchar, // 83 (S)
63+
Nmstart | Nmchar, // 84 (T)
64+
Nmstart | Nmchar, // 85 (U)
65+
Nmstart | Nmchar, // 86 (V)
66+
Nmstart | Nmchar, // 87 (W)
67+
Nmstart | Nmchar, // 88 (X)
68+
Nmstart | Nmchar, // 89 (Y)
69+
Nmstart | Nmchar, // 90 (Z)
70+
0, // 91
71+
Nmstart, // 92 (\)
72+
0, 0, // 93-94
73+
Nmstart | Nmchar, // 95 (_)
74+
0, // 96
75+
Nmstart | Nmchar | Hex, // 97 (a)
76+
Nmstart | Nmchar | Hex, // 98 (b)
77+
Nmstart | Nmchar | Hex, // 99 (c)
78+
Nmstart | Nmchar | Hex, // 100 (d)
79+
Nmstart | Nmchar | Hex, // 101 (e)
80+
Nmstart | Nmchar | Hex, // 102 (f)
81+
Nmstart | Nmchar, // 103 (g)
82+
Nmstart | Nmchar, // 104 (h)
83+
Nmstart | Nmchar, // 105 (i)
84+
Nmstart | Nmchar, // 106 (j)
85+
Nmstart | Nmchar, // 107 (k)
86+
Nmstart | Nmchar, // 108 (l)
87+
Nmstart | Nmchar, // 109 (m)
88+
Nmstart | Nmchar, // 110 (n)
89+
Nmstart | Nmchar, // 111 (o)
90+
Nmstart | Nmchar, // 112 (p)
91+
Nmstart | Nmchar, // 113 (q)
92+
Nmstart | Nmchar, // 114 (r)
93+
Nmstart | Nmchar, // 115 (s)
94+
Nmstart | Nmchar, // 116 (t)
95+
Nmstart | Nmchar, // 117 (u)
96+
Nmstart | Nmchar, // 118 (v)
97+
Nmstart | Nmchar, // 119 (w)
98+
Nmstart | Nmchar, // 120 (x)
99+
Nmstart | Nmchar, // 121 (y)
100+
Nmstart | Nmchar, // 122 (z)
101+
0, 0, 0, 0, 0, // 123-127
102+
// Non-ASCII
103+
Nmstart | Nmchar, // 128
104+
Nmstart | Nmchar, // 129
105+
Nmstart | Nmchar, // 130
106+
Nmstart | Nmchar, // 131
107+
Nmstart | Nmchar, // 132
108+
Nmstart | Nmchar, // 133
109+
Nmstart | Nmchar, // 134
110+
Nmstart | Nmchar, // 135
111+
Nmstart | Nmchar, // 136
112+
Nmstart | Nmchar, // 137
113+
Nmstart | Nmchar, // 138
114+
Nmstart | Nmchar, // 139
115+
Nmstart | Nmchar, // 140
116+
Nmstart | Nmchar, // 141
117+
Nmstart | Nmchar, // 142
118+
Nmstart | Nmchar, // 143
119+
Nmstart | Nmchar, // 144
120+
Nmstart | Nmchar, // 145
121+
Nmstart | Nmchar, // 146
122+
Nmstart | Nmchar, // 147
123+
Nmstart | Nmchar, // 148
124+
Nmstart | Nmchar, // 149
125+
Nmstart | Nmchar, // 150
126+
Nmstart | Nmchar, // 151
127+
Nmstart | Nmchar, // 152
128+
Nmstart | Nmchar, // 153
129+
Nmstart | Nmchar, // 154
130+
Nmstart | Nmchar, // 155
131+
Nmstart | Nmchar, // 156
132+
Nmstart | Nmchar, // 157
133+
Nmstart | Nmchar, // 158
134+
Nmstart | Nmchar, // 159
135+
Nmstart | Nmchar, // 160
136+
Nmstart | Nmchar, // 161
137+
Nmstart | Nmchar, // 162
138+
Nmstart | Nmchar, // 163
139+
Nmstart | Nmchar, // 164
140+
Nmstart | Nmchar, // 165
141+
Nmstart | Nmchar, // 166
142+
Nmstart | Nmchar, // 167
143+
Nmstart | Nmchar, // 168
144+
Nmstart | Nmchar, // 169
145+
Nmstart | Nmchar, // 170
146+
Nmstart | Nmchar, // 171
147+
Nmstart | Nmchar, // 172
148+
Nmstart | Nmchar, // 173
149+
Nmstart | Nmchar, // 174
150+
Nmstart | Nmchar, // 175
151+
Nmstart | Nmchar, // 176
152+
Nmstart | Nmchar, // 177
153+
Nmstart | Nmchar, // 178
154+
Nmstart | Nmchar, // 179
155+
Nmstart | Nmchar, // 180
156+
Nmstart | Nmchar, // 181
157+
Nmstart | Nmchar, // 182
158+
Nmstart | Nmchar, // 183
159+
Nmstart | Nmchar, // 184
160+
Nmstart | Nmchar, // 185
161+
Nmstart | Nmchar, // 186
162+
Nmstart | Nmchar, // 187
163+
Nmstart | Nmchar, // 188
164+
Nmstart | Nmchar, // 189
165+
Nmstart | Nmchar, // 190
166+
Nmstart | Nmchar, // 191
167+
Nmstart | Nmchar, // 192
168+
Nmstart | Nmchar, // 193
169+
Nmstart | Nmchar, // 194
170+
Nmstart | Nmchar, // 195
171+
Nmstart | Nmchar, // 196
172+
Nmstart | Nmchar, // 197
173+
Nmstart | Nmchar, // 198
174+
Nmstart | Nmchar, // 199
175+
Nmstart | Nmchar, // 200
176+
Nmstart | Nmchar, // 201
177+
Nmstart | Nmchar, // 202
178+
Nmstart | Nmchar, // 203
179+
Nmstart | Nmchar, // 204
180+
Nmstart | Nmchar, // 205
181+
Nmstart | Nmchar, // 206
182+
Nmstart | Nmchar, // 207
183+
Nmstart | Nmchar, // 208
184+
Nmstart | Nmchar, // 209
185+
Nmstart | Nmchar, // 210
186+
Nmstart | Nmchar, // 211
187+
Nmstart | Nmchar, // 212
188+
Nmstart | Nmchar, // 213
189+
Nmstart | Nmchar, // 214
190+
Nmstart | Nmchar, // 215
191+
Nmstart | Nmchar, // 216
192+
Nmstart | Nmchar, // 217
193+
Nmstart | Nmchar, // 218
194+
Nmstart | Nmchar, // 219
195+
Nmstart | Nmchar, // 220
196+
Nmstart | Nmchar, // 221
197+
Nmstart | Nmchar, // 222
198+
Nmstart | Nmchar, // 223
199+
Nmstart | Nmchar, // 224
200+
Nmstart | Nmchar, // 225
201+
Nmstart | Nmchar, // 226
202+
Nmstart | Nmchar, // 227
203+
Nmstart | Nmchar, // 228
204+
Nmstart | Nmchar, // 229
205+
Nmstart | Nmchar, // 230
206+
Nmstart | Nmchar, // 231
207+
Nmstart | Nmchar, // 232
208+
Nmstart | Nmchar, // 233
209+
Nmstart | Nmchar, // 234
210+
Nmstart | Nmchar, // 235
211+
Nmstart | Nmchar, // 236
212+
Nmstart | Nmchar, // 237
213+
Nmstart | Nmchar, // 238
214+
Nmstart | Nmchar, // 239
215+
Nmstart | Nmchar, // 240
216+
Nmstart | Nmchar, // 241
217+
Nmstart | Nmchar, // 242
218+
Nmstart | Nmchar, // 243
219+
Nmstart | Nmchar, // 244
220+
Nmstart | Nmchar, // 245
221+
Nmstart | Nmchar, // 246
222+
Nmstart | Nmchar, // 247
223+
Nmstart | Nmchar, // 248
224+
Nmstart | Nmchar, // 249
225+
Nmstart | Nmchar, // 250
226+
Nmstart | Nmchar, // 251
227+
Nmstart | Nmchar, // 252
228+
Nmstart | Nmchar, // 253
229+
Nmstart | Nmchar, // 254
230+
Nmstart | Nmchar // 255
231+
};

‎src/FontParser.cc

+605
Large diffs are not rendered by default.

‎src/FontParser.h

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <vector>
5+
#include <optional>
6+
#include <memory>
7+
#include <variant>
8+
#include <unordered_map>
9+
#include "CharData.h"
10+
11+
enum class FontStyle {
12+
Normal,
13+
Italic,
14+
Oblique
15+
};
16+
17+
enum class FontVariant {
18+
Normal,
19+
SmallCaps
20+
};
21+
22+
struct FontProperties {
23+
double fontSize{16.0f};
24+
std::vector<std::string> fontFamily;
25+
uint16_t fontWeight{400};
26+
FontVariant fontVariant{FontVariant::Normal};
27+
FontStyle fontStyle{FontStyle::Normal};
28+
};
29+
30+
class Token {
31+
public:
32+
enum class Type {
33+
Invalid,
34+
Number,
35+
Percent,
36+
Identifier,
37+
Slash,
38+
Comma,
39+
QuotedString,
40+
Whitespace,
41+
EndOfInput
42+
};
43+
44+
Token(Type type, std::string value);
45+
Token(Type type, double value);
46+
Token(Type type);
47+
48+
Type type() const { return type_; }
49+
50+
const std::string& getString() const;
51+
double getNumber() const;
52+
53+
private:
54+
Type type_;
55+
std::variant<std::string, double> value_;
56+
};
57+
58+
class Tokenizer {
59+
public:
60+
Tokenizer(std::string_view input);
61+
Token nextToken();
62+
63+
private:
64+
std::string_view input_;
65+
size_t position_{0};
66+
67+
// Util
68+
std::string utf8Encode(uint32_t codepoint);
69+
inline bool isWhitespace(char c) const {
70+
return charData[static_cast<uint8_t>(c)] & CharData::Whitespace;
71+
}
72+
inline bool isNewline(char c) const {
73+
return charData[static_cast<uint8_t>(c)] & CharData::Newline;
74+
}
75+
76+
// Moving through the string
77+
char peek() const;
78+
char advance();
79+
80+
// Tokenize
81+
Token parseNumber();
82+
Token parseIdentifier();
83+
uint32_t parseUnicode();
84+
bool parseEscape(std::string& str);
85+
Token parseString(char quote);
86+
};
87+
88+
class FontParser {
89+
public:
90+
static FontProperties parse(const std::string& fontString, bool* success = nullptr);
91+
92+
private:
93+
static const std::unordered_map<std::string, uint16_t> weightMap;
94+
static const std::unordered_map<std::string, double> unitMap;
95+
96+
FontParser(std::string_view input);
97+
98+
void advance();
99+
void skipWs();
100+
bool check(Token::Type type) const;
101+
bool checkWs() const;
102+
103+
bool parseFontStyle(FontProperties& props);
104+
bool parseFontVariant(FontProperties& props);
105+
bool parseFontWeight(FontProperties& props);
106+
bool parseFontSize(FontProperties& props);
107+
bool parseLineHeight(FontProperties& props);
108+
bool parseFontFamily(FontProperties& props);
109+
FontProperties parseFont();
110+
111+
Tokenizer tokenizer_;
112+
Token currentToken_;
113+
Token nextToken_;
114+
bool hasError_{false};
115+
};

‎test/canvas.test.js

-73
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ const {
1414
createCanvas,
1515
createImageData,
1616
loadImage,
17-
parseFont,
1817
registerFont,
1918
Canvas,
2019
deregisterAllFonts
@@ -37,78 +36,6 @@ describe('Canvas', function () {
3736
assert('width' in Canvas.prototype)
3837
})
3938

40-
it('.parseFont()', function () {
41-
const tests = [
42-
'20px Arial',
43-
{ size: 20, unit: 'px', family: 'Arial' },
44-
'20pt Arial',
45-
{ size: 26.666666666666668, unit: 'pt', family: 'Arial' },
46-
'20.5pt Arial',
47-
{ size: 27.333333333333332, unit: 'pt', family: 'Arial' },
48-
'20% Arial',
49-
{ size: 20, unit: '%', family: 'Arial' }, // TODO I think this is a bad assertion - ZB 23-Jul-2017
50-
'20mm Arial',
51-
{ size: 75.59055118110237, unit: 'mm', family: 'Arial' },
52-
'20px serif',
53-
{ size: 20, unit: 'px', family: 'serif' },
54-
'20px sans-serif',
55-
{ size: 20, unit: 'px', family: 'sans-serif' },
56-
'20px monospace',
57-
{ size: 20, unit: 'px', family: 'monospace' },
58-
'50px Arial, sans-serif',
59-
{ size: 50, unit: 'px', family: 'Arial,sans-serif' },
60-
'bold italic 50px Arial, sans-serif',
61-
{ style: 'italic', weight: 'bold', size: 50, unit: 'px', family: 'Arial,sans-serif' },
62-
'50px Helvetica , Arial, sans-serif',
63-
{ size: 50, unit: 'px', family: 'Helvetica,Arial,sans-serif' },
64-
'50px "Helvetica Neue", sans-serif',
65-
{ size: 50, unit: 'px', family: 'Helvetica Neue,sans-serif' },
66-
'50px "Helvetica Neue", "foo bar baz" , sans-serif',
67-
{ size: 50, unit: 'px', family: 'Helvetica Neue,foo bar baz,sans-serif' },
68-
"50px 'Helvetica Neue'",
69-
{ size: 50, unit: 'px', family: 'Helvetica Neue' },
70-
'italic 20px Arial',
71-
{ size: 20, unit: 'px', style: 'italic', family: 'Arial' },
72-
'oblique 20px Arial',
73-
{ size: 20, unit: 'px', style: 'oblique', family: 'Arial' },
74-
'normal 20px Arial',
75-
{ size: 20, unit: 'px', style: 'normal', family: 'Arial' },
76-
'300 20px Arial',
77-
{ size: 20, unit: 'px', weight: '300', family: 'Arial' },
78-
'800 20px Arial',
79-
{ size: 20, unit: 'px', weight: '800', family: 'Arial' },
80-
'bolder 20px Arial',
81-
{ size: 20, unit: 'px', weight: 'bolder', family: 'Arial' },
82-
'lighter 20px Arial',
83-
{ size: 20, unit: 'px', weight: 'lighter', family: 'Arial' },
84-
'normal normal normal 16px Impact',
85-
{ size: 16, unit: 'px', weight: 'normal', family: 'Impact', style: 'normal', variant: 'normal' },
86-
'italic small-caps bolder 16px cursive',
87-
{ size: 16, unit: 'px', style: 'italic', variant: 'small-caps', weight: 'bolder', family: 'cursive' },
88-
'20px "new century schoolbook", serif',
89-
{ size: 20, unit: 'px', family: 'new century schoolbook,serif' },
90-
'20px "Arial bold 300"', // synthetic case with weight keyword inside family
91-
{ size: 20, unit: 'px', family: 'Arial bold 300', variant: 'normal' },
92-
`50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`,
93-
{ size: 50, unit: 'px', family: `Helvetica 'Neue',foo "bar" baz,Someone's weird 'edge' case,sans-serif` }
94-
]
95-
96-
for (let i = 0, len = tests.length; i < len; ++i) {
97-
const str = tests[i++]
98-
const expected = tests[i]
99-
const actual = parseFont(str)
100-
101-
if (!expected.style) expected.style = 'normal'
102-
if (!expected.weight) expected.weight = 'normal'
103-
if (!expected.stretch) expected.stretch = 'normal'
104-
if (!expected.variant) expected.variant = 'normal'
105-
106-
assert.deepEqual(actual, expected, 'Failed to parse: ' + str)
107-
}
108-
109-
assert.strictEqual(parseFont('Helvetica, sans'), undefined)
110-
})
111-
11239
it('registerFont', function () {
11340
// Minimal test to make sure nothing is thrown
11441
registerFont('./examples/pfennigFont/Pfennig.ttf', { family: 'Pfennig' })

‎test/fontParser.test.js

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/* eslint-env mocha */
2+
3+
'use strict'
4+
5+
/**
6+
* Module dependencies.
7+
*/
8+
const assert = require('assert')
9+
const {Canvas} = require('..');
10+
11+
const tests = [
12+
'20px Arial',
13+
{ size: 20, families: ['arial'] },
14+
'20pt Arial',
15+
{ size: 26.666667461395264, families: ['arial'] },
16+
'20.5pt Arial',
17+
{ size: 27.333334147930145, families: ['arial'] },
18+
'20% Arial',
19+
{ size: 3.1999999284744263, families: ['arial'] },
20+
'20mm Arial',
21+
{ size: 75.59999942779541, families: ['arial'] },
22+
'20px serif',
23+
{ size: 20, families: ['serif'] },
24+
'20px sans-serif',
25+
{ size: 20, families: ['sans-serif'] },
26+
'20px monospace',
27+
{ size: 20, families: ['monospace'] },
28+
'50px Arial, sans-serif',
29+
{ size: 50, families: ['arial', 'sans-serif'] },
30+
'bold italic 50px Arial, sans-serif',
31+
{ style: 1, weight: 700, size: 50, families: ['arial', 'sans-serif'] },
32+
'50px Helvetica , Arial, sans-serif',
33+
{ size: 50, families: ['helvetica', 'arial', 'sans-serif'] },
34+
'50px "Helvetica Neue", sans-serif',
35+
{ size: 50, families: ['Helvetica Neue', 'sans-serif'] },
36+
'50px "Helvetica Neue", "foo bar baz" , sans-serif',
37+
{ size: 50, families: ['Helvetica Neue', 'foo bar baz', 'sans-serif'] },
38+
"50px 'Helvetica Neue'",
39+
{ size: 50, families: ['Helvetica Neue'] },
40+
'italic 20px Arial',
41+
{ size: 20, style: 1, families: ['arial'] },
42+
'oblique 20px Arial',
43+
{ size: 20, style: 2, families: ['arial'] },
44+
'normal 20px Arial',
45+
{ size: 20, families: ['arial'] },
46+
'300 20px Arial',
47+
{ size: 20, weight: 300, families: ['arial'] },
48+
'800 20px Arial',
49+
{ size: 20, weight: 800, families: ['arial'] },
50+
'bolder 20px Arial',
51+
{ size: 20, weight: 700, families: ['arial'] },
52+
'lighter 20px Arial',
53+
{ size: 20, weight: 100, families: ['arial'] },
54+
'normal normal normal 16px Impact',
55+
{ size: 16, families: ['impact'] },
56+
'italic small-caps bolder 16px cursive',
57+
{ size: 16, style: 1, variant: 1, weight: 700, families: ['cursive'] },
58+
'20px "new century schoolbook", serif',
59+
{ size: 20, families: ['new century schoolbook', 'serif'] },
60+
'20px "Arial bold 300"', // synthetic case with weight keyword inside family
61+
{ size: 20, families: ['Arial bold 300'] },
62+
`50px "Helvetica 'Neue'", "foo \\"bar\\" baz" , "Someone's weird \\'edge\\' case", sans-serif`,
63+
{ size: 50, families: [`Helvetica 'Neue'`, 'foo "bar" baz', `Someone's weird 'edge' case`, 'sans-serif'] },
64+
'Helvetica, sans',
65+
undefined,
66+
'123px thefont/123abc',
67+
undefined,
68+
'123px /\tnormal thefont',
69+
{size: 123, families: ['thefont']},
70+
'12px/1.2whoops arial',
71+
undefined,
72+
'bold bold 12px thefont',
73+
undefined,
74+
'italic italic 12px Arial',
75+
undefined,
76+
'small-caps bold italic small-caps 12px Arial',
77+
undefined,
78+
'small-caps bold oblique 12px \'A\'ri\\61l',
79+
{size: 12, style: 2, weight: 700, variant: 1, families: ['Arial']},
80+
'12px/34% "The\\\n Word"',
81+
{size: 12, families: ['The Word']},
82+
'',
83+
undefined,
84+
'normal normal normal 1%/normal a , \'b\'',
85+
{size: 0.1599999964237213, families: ['a', 'b']},
86+
'normalnormalnormal 1px/normal a',
87+
undefined,
88+
'12px _the_font',
89+
{size: 12, families: ['_the_font']},
90+
'9px 7 birds',
91+
undefined,
92+
'2em "Courier',
93+
undefined,
94+
`2em \\'Courier\\"`,
95+
{size: 32, families: ['\'courier"']},
96+
'1px \\10abcde',
97+
{size: 1, families: [String.fromCodePoint(parseInt('10abcd', 16)) + 'e']},
98+
'3E+2 1e-1px yay',
99+
{weight: 300, size: 0.1, families: ['yay']}
100+
];
101+
102+
describe('Font parser', function () {
103+
for (let i = 0; i < tests.length; i++) {
104+
const str = tests[i++]
105+
it(str, function () {
106+
const expected = tests[i]
107+
const actual = Canvas.parseFont(str)
108+
109+
if (expected) {
110+
if (expected.style == null) expected.style = 0
111+
if (expected.weight == null) expected.weight = 400
112+
if (expected.variant == null) expected.variant = 0
113+
}
114+
115+
assert.deepEqual(actual, expected)
116+
})
117+
}
118+
})

0 commit comments

Comments
 (0)
Please sign in to comment.