From 40a1fdc6afd6b513d4c26333580c90fe6a080c7b Mon Sep 17 00:00:00 2001 From: Kyle Dickerson Date: Wed, 29 May 2024 22:01:29 -0700 Subject: [PATCH] Implement basic kerning when rendering text. Only old-style `kern` tables are supported--modern GPOS-based kerning is not supported. --- src/describescreen.cpp | 16 +++++++++++++++- src/drawentity.cpp | 3 ++- src/request.cpp | 3 ++- src/ttf.cpp | 29 +++++++++++++++++++++++------ src/ttf.h | 8 ++++---- src/ui.h | 1 + test/request/ttf_text/kerning.png | Bin 0 -> 6122 bytes test/request/ttf_text/kerning.slvs | Bin 0 -> 5932 bytes test/request/ttf_text/test.cpp | 6 ++++++ 9 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 test/request/ttf_text/kerning.png create mode 100644 test/request/ttf_text/kerning.slvs diff --git a/src/describescreen.cpp b/src/describescreen.cpp index dd541ee..0659034 100644 --- a/src/describescreen.cpp +++ b/src/describescreen.cpp @@ -19,6 +19,17 @@ void TextWindow::ScreenEditTtfText(int link, uint32_t v) { SS.TW.edit.request = hr; } +void TextWindow::ScreenToggleTtfKerning(int link, uint32_t v) { + hRequest hr = { v }; + Request *r = SK.GetRequest(hr); + + SS.UndoRemember(); + r->extraPoints = !r->extraPoints; + + SS.MarkGroupDirty(r->group); + SS.ScheduleShowTW(); +} + void TextWindow::ScreenSetTtfFont(int link, uint32_t v) { int i = (int)v; if(i < 0) return; @@ -205,8 +216,11 @@ void TextWindow::DescribeSelection() { Printf(false, "%FtTRUETYPE FONT TEXT%E"); Printf(true, " font = '%Fi%s%E'", e->font.c_str()); if(e->h.isFromRequest()) { - Printf(false, " text = '%Fi%s%E' %Fl%Ll%f%D[change]%E", + Printf(true, " text = '%Fi%s%E' %Fl%Ll%f%D[change]%E", e->str.c_str(), &ScreenEditTtfText, e->h.request().v); + Printf(true, " %Fd%f%D%Ll%s apply kerning", + &ScreenToggleTtfKerning, e->h.request().v, + e->extraPoints ? CHECK_TRUE : CHECK_FALSE); Printf(true, " select new font"); SS.fonts.LoadAll(); // Not using range-for here because we use i inside the output. diff --git a/src/drawentity.cpp b/src/drawentity.cpp index 2dbbe54..7ac0338 100644 --- a/src/drawentity.cpp +++ b/src/drawentity.cpp @@ -475,7 +475,8 @@ void Entity::GenerateBezierCurves(SBezierList *sbl) const { Vector v = topLeft.Minus(botLeft); Vector u = (v.Cross(n)).WithMagnitude(v.Magnitude()); - SS.fonts.PlotString(font, str, sbl, botLeft, u, v); + // `extraPoints` is storing kerning boolean + SS.fonts.PlotString(font, str, sbl, extraPoints, botLeft, u, v); break; } diff --git a/src/request.cpp b/src/request.cpp index 3614cec..7613a03 100644 --- a/src/request.cpp +++ b/src/request.cpp @@ -90,7 +90,8 @@ void Request::Generate(IdList *entity, // Request-specific generation. switch(type) { case Type::TTF_TEXT: { - double actualAspectRatio = SS.fonts.AspectRatio(font, str); + // `extraPoints` is storing kerning boolean + double actualAspectRatio = SS.fonts.AspectRatio(font, str, extraPoints); if(EXACT(actualAspectRatio != 0.0)) { // We could load the font, so use the actual value. aspectRatio = actualAspectRatio; diff --git a/src/ttf.cpp b/src/ttf.cpp index e39afc7..3d7cb63 100644 --- a/src/ttf.cpp +++ b/src/ttf.cpp @@ -108,11 +108,11 @@ TtfFont *TtfFontList::LoadFont(const std::string &font) } void TtfFontList::PlotString(const std::string &font, const std::string &str, - SBezierList *sbl, Vector origin, Vector u, Vector v) + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v) { TtfFont *tf = LoadFont(font); if(!str.empty() && tf != NULL) { - tf->PlotString(str, sbl, origin, u, v); + tf->PlotString(str, sbl, kerning, origin, u, v); } else { // No text or no font; so draw a big X for an error marker. SBezier sb; @@ -123,11 +123,11 @@ void TtfFontList::PlotString(const std::string &font, const std::string &str, } } -double TtfFontList::AspectRatio(const std::string &font, const std::string &str) +double TtfFontList::AspectRatio(const std::string &font, const std::string &str, bool kerning) { TtfFont *tf = LoadFont(font); if(tf != NULL) { - return tf->AspectRatio(str); + return tf->AspectRatio(str, kerning); } return 0.0; @@ -331,7 +331,7 @@ static int CubicTo(const FT_Vector *c1, const FT_Vector *c2, const FT_Vector *p, } void TtfFont::PlotString(const std::string &str, - SBezierList *sbl, Vector origin, Vector u, Vector v) + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v) { ssassert(fontFace != NULL, "Expected font face to be loaded"); @@ -344,6 +344,7 @@ void TtfFont::PlotString(const std::string &str, outlineFuncs.delta = 0; FT_Pos dx = 0; + uint32_t prevGid = 0; for(char32_t cid : ReadUTF8(str)) { uint32_t gid = FT_Get_Char_Index(fontFace, cid); if (gid == 0) { @@ -382,6 +383,13 @@ void TtfFont::PlotString(const std::string &str, */ FT_BBox cbox; FT_Outline_Get_CBox(&fontFace->glyph->outline, &cbox); + + // Apply Kerning, if any: + FT_Vector kernVector; + if(kerning && FT_Get_Kerning(fontFace, prevGid, gid, FT_KERNING_DEFAULT, &kernVector) == 0) { + dx += kernVector.x; + } + FT_Pos bx = dx - cbox.xMin; // Yes, this is what FreeType calls left-side bearing. // Then interchangeably uses that with "left-side bearing". Sigh. @@ -402,14 +410,16 @@ void TtfFont::PlotString(const std::string &str, // And we're done, so advance our position by the requested advance // width, plus the user-requested extra advance. dx += fontFace->glyph->advance.x; + prevGid = gid; } } -double TtfFont::AspectRatio(const std::string &str) { +double TtfFont::AspectRatio(const std::string &str, bool kerning) { ssassert(fontFace != NULL, "Expected font face to be loaded"); // We always request a unit size character, so the aspect ratio is the same as advance length. double dx = 0; + uint32_t prevGid = 0; for(char32_t chr : ReadUTF8(str)) { uint32_t gid = FT_Get_Char_Index(fontFace, chr); if (gid == 0) { @@ -424,7 +434,14 @@ double TtfFont::AspectRatio(const std::string &str) { break; } + // Apply Kerning, if any: + FT_Vector kernVector; + if(kerning && FT_Get_Kerning(fontFace, prevGid, gid, FT_KERNING_DEFAULT, &kernVector) == 0) { + dx += (double)kernVector.x / capHeight; + } + dx += (double)fontFace->glyph->advance.x / capHeight; + prevGid = gid; } return dx; diff --git a/src/ttf.h b/src/ttf.h index 565e6a9..1dc7205 100644 --- a/src/ttf.h +++ b/src/ttf.h @@ -24,8 +24,8 @@ public: bool LoadFromResource(FT_LibraryRec_ *fontLibrary, bool keepOpen = false); void PlotString(const std::string &str, - SBezierList *sbl, Vector origin, Vector u, Vector v); - double AspectRatio(const std::string &str); + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v); + double AspectRatio(const std::string &str, bool kerning); bool ExtractTTFData(bool keepOpen); }; @@ -43,8 +43,8 @@ public: TtfFont *LoadFont(const std::string &font); void PlotString(const std::string &font, const std::string &str, - SBezierList *sbl, Vector origin, Vector u, Vector v); - double AspectRatio(const std::string &font, const std::string &str); + SBezierList *sbl, bool kerning, Vector origin, Vector u, Vector v); + double AspectRatio(const std::string &font, const std::string &str, bool kerning); }; #endif diff --git a/src/ui.h b/src/ui.h index 5e40194..49c1be6 100644 --- a/src/ui.h +++ b/src/ui.h @@ -402,6 +402,7 @@ public: // All of these are callbacks from the GUI code; first from when // we're describing an entity static void ScreenEditTtfText(int link, uint32_t v); + static void ScreenToggleTtfKerning(int link, uint32_t v); static void ScreenSetTtfFont(int link, uint32_t v); static void ScreenUnselectAll(int link, uint32_t v); diff --git a/test/request/ttf_text/kerning.png b/test/request/ttf_text/kerning.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6eab03574b83cf80bdde49751ae18e19dc4e7a GIT binary patch literal 6122 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 Qda8(6_(;+eUT5Ne0p|4z^Z)<= literal 0 HcmV?d00001 diff --git a/test/request/ttf_text/test.cpp b/test/request/ttf_text/test.cpp index 5e9db1b..2937f79 100644 --- a/test/request/ttf_text/test.cpp +++ b/test/request/ttf_text/test.cpp @@ -6,6 +6,12 @@ TEST_CASE(normal_roundtrip) { CHECK_SAVE("normal.slvs"); } +TEST_CASE(kerning_roundtrip) { + CHECK_LOAD("kerning.slvs"); + CHECK_RENDER("kerning.png"); + CHECK_SAVE("kerning.slvs"); +} + TEST_CASE(normal_migrate_from_v20) { CHECK_LOAD("normal_v20.slvs"); CHECK_SAVE("normal.slvs");