From a0c97a102e5f926323ab144dd532c4e6b8a7811f Mon Sep 17 00:00:00 2001 From: Benjamin Nauck Date: Sun, 8 Mar 2026 20:56:23 +0100 Subject: [PATCH] Part: Add HarfBuzz text shaping for proper kerning in makeTextWires Replace the character-by-character positioning loop with HarfBuzz text shaping. The old code used FT_Get_Kerning which only reads legacy kern tables. HarfBuzz handles both GPOS kerning and legacy kern tables, as well as ligatures and complex script shaping. --- cMake/FreeCAD_Helpers/SetupFreeType.cmake | 16 ++++++ src/Mod/Part/App/CMakeLists.txt | 2 + src/Mod/Part/App/Geometry.cpp | 62 ++++++++++++++--------- 3 files changed, 56 insertions(+), 24 deletions(-) diff --git a/cMake/FreeCAD_Helpers/SetupFreeType.cmake b/cMake/FreeCAD_Helpers/SetupFreeType.cmake index a4b669628d..e4440f21cd 100644 --- a/cMake/FreeCAD_Helpers/SetupFreeType.cmake +++ b/cMake/FreeCAD_Helpers/SetupFreeType.cmake @@ -7,6 +7,22 @@ macro(SetupFreetype) message("===============================================================\n" "FreeType2 not found. Part module will lack of makeWireString().\n" "===============================================================\n") + else() + # find_package(harfbuzz CONFIG) fails on windows + # lets do it the complicated way instead. + find_path(HARFBUZZ_INCLUDE_DIR hb.h PATH_SUFFIXES harfbuzz) + find_library(HARFBUZZ_LIBRARY NAMES harfbuzz) + if(HARFBUZZ_INCLUDE_DIR AND HARFBUZZ_LIBRARY) + if(NOT TARGET harfbuzz::harfbuzz) + add_library(harfbuzz::harfbuzz UNKNOWN IMPORTED) + set_target_properties(harfbuzz::harfbuzz PROPERTIES + IMPORTED_LOCATION "${HARFBUZZ_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${HARFBUZZ_INCLUDE_DIR}" + ) + endif() + else() + message(FATAL_ERROR "HarfBuzz not found") + endif() endif(NOT FREETYPE_FOUND) endif(FREECAD_USE_FREETYPE) diff --git a/src/Mod/Part/App/CMakeLists.txt b/src/Mod/Part/App/CMakeLists.txt index 23b487ff07..5b4aa24989 100644 --- a/src/Mod/Part/App/CMakeLists.txt +++ b/src/Mod/Part/App/CMakeLists.txt @@ -41,6 +41,8 @@ if(FREETYPE_FOUND) ${Part_LIBS} ${FREETYPE_LIBRARIES} ) + + set(Part_LIBS ${Part_LIBS} harfbuzz::harfbuzz) endif(FREETYPE_FOUND) generate_from_py(Arc) diff --git a/src/Mod/Part/App/Geometry.cpp b/src/Mod/Part/App/Geometry.cpp index b716190a0d..22f96b4eaf 100644 --- a/src/Mod/Part/App/Geometry.cpp +++ b/src/Mod/Part/App/Geometry.cpp @@ -126,8 +126,8 @@ #include FT_FREETYPE_H #include FT_GLYPH_H #include FT_OUTLINE_H -#include -#include + +#include // headers to scale text correctly #include @@ -7659,32 +7659,42 @@ std::vector makeTextWires( FT_Outline_Funcs ftCallbacks = {move_cb, line_cb, quad_cb, cubic_cb, 0, 0}; FT_UInt ftLoadFlags = FT_LOAD_NO_SCALE | FT_LOAD_NO_BITMAP; + // Use HarfBuzz for text shaping. Positions are in font design units (upem), + // matching our FT_LOAD_NO_SCALE outline decomposition. + auto* hbBlob = hb_blob_create( + fontBuffer.data(), + static_cast(fontBuffer.size()), + HB_MEMORY_MODE_READONLY, + nullptr, + nullptr + ); + auto* hbFace = hb_face_create(hbBlob, 0); + auto* hbFont = hb_font_create(hbFace); + + auto* hbBuf = hb_buffer_create(); + hb_buffer_add_utf8(hbBuf, text.c_str(), -1, 0, -1); + hb_buffer_guess_segment_properties(hbBuf); + + hb_shape(hbFont, hbBuf, nullptr, 0); + + unsigned int glyphCount = 0; + auto* glyphInfos = hb_buffer_get_glyph_infos(hbBuf, &glyphCount); + auto* glyphPositions = hb_buffer_get_glyph_positions(hbBuf, &glyphCount); + double penPos = 0.0; double currentTracking = 0.0; - FT_ULong prevCharcode = 0; - std::wstring_convert, char32_t> converter; - std::u32string wide_text = converter.from_bytes(text); + for (unsigned int i = 0; i < glyphCount; ++i) { + FT_UInt glyphIndex = glyphInfos[i].codepoint; + double xOffset = glyphPositions[i].x_offset; + double xAdvance = glyphPositions[i].x_advance; - for (size_t i = 0; i < wide_text.length(); ++i) { - FT_ULong charcode = wide_text[i]; - - if (FT_Load_Char(ftFace, charcode, ftLoadFlags) != 0) { + if (FT_Load_Glyph(ftFace, glyphIndex, ftLoadFlags) != 0) { + penPos += xAdvance; + currentTracking += tracking; continue; } - if (prevCharcode != 0 && FT_HAS_KERNING(ftFace)) { - FT_Vector kern; - FT_Get_Kerning( - ftFace, - FT_Get_Char_Index(ftFace, prevCharcode), - FT_Get_Char_Index(ftFace, charcode), - FT_KERNING_UNSCALED, - &kern - ); - penPos += kern.x; - } - if (ftFace->glyph->format == FT_GLYPH_FORMAT_OUTLINE && ftFace->glyph->outline.n_contours > 0) { FTDC_Ctx ctx; @@ -7696,7 +7706,7 @@ std::vector makeTextWires( if (!ctx.Wires.empty()) { gp_Trsf charTransform; charTransform.SetScale(gp::Origin(), scaleFactor); - gp_Vec translation(penPos * scaleFactor + currentTracking, 0.0, 0.0); + gp_Vec translation((penPos + xOffset) * scaleFactor + currentTracking, 0.0, 0.0); charTransform.SetTranslationPart(translation); for (const auto& wire : ctx.Wires) { @@ -7709,11 +7719,15 @@ std::vector makeTextWires( } } - penPos += ftFace->glyph->advance.x; + penPos += xAdvance; currentTracking += tracking; - prevCharcode = charcode; } + hb_buffer_destroy(hbBuf); + hb_font_destroy(hbFont); + hb_face_destroy(hbFace); + hb_blob_destroy(hbBlob); + FT_Done_Face(ftFace); FT_Done_FreeType(ftLib); return allWires;