From 1cd27283cc5e2a6fa6286d24462a695c510e84f7 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 19 Aug 2026 21:34:55 -0400 Subject: [PATCH 1/2] faster fill/strokeText by holding FT_Face I noticed that cairo_show_glyphs was slower vs when Pango calls it. Pango holds the FT_Face until the process ends. node-canvas now keeps it until the thread/InstanceData is deleted. Seems not great to me, but if Pango does it that way, let's wait until it becomes a problem. --- src/CanvasRenderingContext2d.cc | 28 +-------------------------- src/Font.h | 34 +++++++++++++++++++++++++++++++-- src/FontLayout.cc | 2 +- src/InstanceData.h | 24 ++++++++++++++--------- 4 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 6183837c0..4dfebed82 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2457,33 +2457,7 @@ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { double fontSize = state->fontProperties.size; double toPx = fontSize / 1000; - FT_Face ftface; - FT_Error newFaceResult = FT_New_Memory_Face( - data->ft, - reinterpret_cast(run.face->data.get()), - run.face->data_len, - run.face->index, - &ftface - ); - - if (newFaceResult != 0) continue; - - cairo_font_face_t* crface = cairo_ft_font_face_create_for_ft_face(ftface, 0); - - static const cairo_user_data_key_t key{0}; - cairo_status_t setDataResult = cairo_font_face_set_user_data( - crface, - &key, - ftface, - (cairo_destroy_func_t) FT_Done_Face - ); - - if (setDataResult) { - cairo_font_face_destroy(crface); - FT_Done_Face (ftface); - continue; - } - + cairo_font_face_t* crface = cairo_ft_font_face_create_for_ft_face(run.face->ftface.get(), 0); cairo_set_font_face(context(), crface); cairo_set_font_size(context(), fontSize); diff --git a/src/Font.h b/src/Font.h index 095ffaf7f..849b54710 100644 --- a/src/Font.h +++ b/src/Font.h @@ -8,6 +8,8 @@ #include #include +#include +#include enum class FontStyle { Normal, @@ -42,6 +44,14 @@ struct HbFontDeleter { } }; +struct FT_FaceDeleter { + void operator()(FT_Face face) { + FT_Done_Face(face); + } +}; + +using FT_Face_Type = std::remove_pointer::type; + // Descriptors describe real fonts on the OS struct FontDescriptor : FontBase { std::unique_ptr family; @@ -53,6 +63,7 @@ struct FontDescriptor : FontBase { std::unique_ptr url = nullptr; std::unique_ptr data = nullptr; std::unique_ptr hbfont = nullptr; + std::unique_ptr ftface = nullptr; size_t data_len = 0; size_t index = 0; FontStatus status = FontStatus::Unloaded; @@ -95,6 +106,22 @@ struct FontDescriptor : FontBase { hb_font_set_scale(hbfont.get(), 1000, 1000); } + void loadFtFont(FT_Library ft) { + FT_Face ftface; + FT_Error newFaceResult = FT_New_Memory_Face( + ft, + reinterpret_cast(data.get()), + data_len, + index, + &ftface + ); + if (newFaceResult) { + status = FontStatus::Error; + } else { + this->ftface = std::unique_ptr(ftface); + } + } + void loadData() { FILE* file = nullptr; long file_size = 0; @@ -134,9 +161,12 @@ struct FontDescriptor : FontBase { if (file) fclose(file); } - void load() { + void load(FT_Library ft) { if (status == FontStatus::Unloaded) loadData(); - if (status == FontStatus::Loaded && hbfont == nullptr) loadHbFont(); + if (status == FontStatus::Loaded && hbfont == nullptr) { + loadHbFont(); + loadFtFont(ft); + } } }; diff --git a/src/FontLayout.cc b/src/FontLayout.cc index 36eb10208..d98c1d82e 100644 --- a/src/FontLayout.cc +++ b/src/FontLayout.cc @@ -417,7 +417,7 @@ layoutText( while (shapingWorkList.size() && matchIndex < matches.size()) { auto face = matches[matchIndex]; - face->load(); + face->load(data->ft()); if (!face->hbfont) { matchIndex++; continue; diff --git a/src/InstanceData.h b/src/InstanceData.h index b8844ffad..12557e9cf 100644 --- a/src/InstanceData.h +++ b/src/InstanceData.h @@ -15,6 +15,16 @@ using PlatformFontManager = FontManagerMacos; using PlatformFontManager = FontManagerLinux; #endif +struct FT_Wrapper { + FT_Library ft; + FT_Wrapper() { + FT_Init_FreeType(&ft); + } + ~FT_Wrapper() { + FT_Done_FreeType(ft); + } +}; + struct InstanceData { Napi::FunctionReference CanvasCtor; Napi::FunctionReference CanvasGradientCtor; @@ -27,14 +37,10 @@ struct InstanceData { Napi::FunctionReference FontFaceCtor; Napi::ObjectReference jsFontSet; FontFaceSet* cppFontSet; - FT_Library ft; + // FT_Wrapper exists because, and it is located here so that, FT_Done_FreeType + // is called after ~PlatformFontManager. FT_Faces are all freed in + // FT_Done_FreeType. That can't happen before their backing buffers are freed. + FT_Wrapper _ft; + inline FT_Library ft() { return _ft.ft; } PlatformFontManager fontManager; - - InstanceData() { - FT_Init_FreeType(&ft); - } - - ~InstanceData() { - FT_Done_FreeType(ft); - } }; From 49ee213ac9a1c04489be7f3e6158406003a0cf43 Mon Sep 17 00:00:00 2001 From: Caleb Hearon Date: Wed, 19 Aug 2026 21:41:17 -0400 Subject: [PATCH 2/2] faster fill/strokeText by avoiding cairo_set_font_size I noticed that cairo_show_glyphs was _still_ slower vs when Pango calls it. Suspecting another instance had caching info attached, I read the Cairo source code to confirm it. --- src/CanvasRenderingContext2d.cc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/CanvasRenderingContext2d.cc b/src/CanvasRenderingContext2d.cc index 4dfebed82..15ee2d564 100644 --- a/src/CanvasRenderingContext2d.cc +++ b/src/CanvasRenderingContext2d.cc @@ -2459,7 +2459,14 @@ Context2d::paintText(const Napi::CallbackInfo& info, bool stroke) { cairo_font_face_t* crface = cairo_ft_font_face_create_for_ft_face(run.face->ftface.get(), 0); cairo_set_font_face(context(), crface); - cairo_set_font_size(context(), fontSize); + + // Be shy about cairo_set_font_size. This always recreates a scaled font, + // which destroys lots of caching. Scaled fonts are associated with the + // font's matrix, which is both for scaling from em to Cairo space and for + // fake italics. Just checking xx should therefore be reliable. + cairo_matrix_t m; + cairo_get_font_matrix(context(), &m); + if (m.xx != fontSize) cairo_set_font_size(context(), fontSize); size_t hbGlyphIndex = 0; while (hbGlyphIndex < run.glyphs.size()) {