Skip to content

Commit 728e76c

Browse files
committed
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, which use random names to bypass the cache, and still a full 2x as fast when the JS version has a cached value. Those results were a bit inconsistent, so I'm not sure how much I trust them, but I expect this parser to have a stable performance profile nonetheless, so I'm not going to add any caching. It's also far more correct than what we had!
1 parent 7ed0a96 commit 728e76c

12 files changed

+1130
-210
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ project adheres to [Semantic Versioning](http://semver.org/).
99
==================
1010
### Changed
1111
* Replaced `simple-get ` with ` Node.js builtin` `fetch` (#2309)
12+
* `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.
13+
1214
### Added
1315
### Fixed
1416

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
/*

0 commit comments

Comments
 (0)