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

0 commit comments

Comments
 (0)
Please sign in to comment.