From f31dc6c9cef5ec3996b8c18844dc53b5f6e7c1d4 Mon Sep 17 00:00:00 2001 From: Ke Shi Date: Tue, 29 Oct 2024 19:39:28 +0800 Subject: [PATCH] [80_3] Enhance sup/subscript kerning via OpenType math table (#2090) ## What We have utilized the italic correction table and kerning table in OpenType to optimize the typesetting of superscripts and subscripts. However, it is important to note that this implementation is non-standard. The italic correction table is used to improve the typesetting on the right side of glyphs. For general characters, the right subscript uses the default position, while the right superscript needs to be shifted to the right by this correction. For symbols like integral signs, the official documentation suggests that both the right superscript and right subscript should be moved by half the offset, while some documents recommend that the superscript remain stationary and the subscript be moved by the full offset. For aesthetic reasons, we have adopted a compromise solution here, where the subscript is shifted left by 0.6 times the offset and the superscript is shifted right by 0.4 times the offset. For the kerning table, it provides correction values for the left and right superscripts and subscripts of a glyph based on their height. According to the OpenType standard recommendations, we should select an optimal correction value by considering both the base glyph and the superscript/subscript glyph at the two recommended correction heights. However, we are unable to handle this in the font's cpp file, so we have opted for a simpler approach, using `fn->y1` and `fn->y2` as the correction heights for the subscript and superscript, respectively, which also yields satisfactory results. Another fact is that many fonts do not provide a kerning table or only provide a very limited kerning table, and almost none of them provide correction values for left superscripts and subscripts. So it can be considered that this PR does not actually change any left-side typesetting. ## Why For certain OpenType fonts, some symbols such as integral signs and superscripts/subscripts appear in incorrect positions. Now, we can improve this by utilizing the metrics provided in OpenType. ## How to test Open `devel/80_3.tmu`. Before: ![image](https://github.com/user-attachments/assets/ff0aef4c-5c5d-4c87-8853-60ef18732134) After: ![image](https://github.com/user-attachments/assets/f3076b05-eae4-4dbc-848e-288340669288) It can be seen that the first two Opentype fonts have been correctly improved, while the latter two have not changed because we used a hardcoded solution. --- devel/80_3.tmu | 63 ++++++++++++ src/Plugins/Freetype/tt_tools.cpp | 85 ++++++++++++++++ src/Plugins/Freetype/tt_tools.hpp | 12 +++ src/Plugins/Freetype/unicode_font.cpp | 139 +++++++++++++++++++++++++- src/Plugins/Freetype/unicode_font.hpp | 5 + 5 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 devel/80_3.tmu diff --git a/devel/80_3.tmu b/devel/80_3.tmu new file mode 100644 index 000000000..c64ed5048 --- /dev/null +++ b/devel/80_3.tmu @@ -0,0 +1,63 @@ +> + + + +<\body> + Asana Math: + + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦> + + <\equation*> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦 + + + \; + + Fira Math: + + <\with|font|Fira Math> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦> + + <\equation*> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦 + + + + \; + + TeX Gyre Pagella Math: + + <\with|font|TeX Gyre Pagella Math> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦> + + <\equation*> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦 + + + \; + + + TeX Gyre Schola Math: + + <\with|font|TeX Gyre Schola Math> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦> + + <\equation*> + 𝑒dx BA UVZZPQ\\\\\𝓐𝓥𝓦 + + + + \; + + \; + + +<\initial> + <\collection> + + + + + + + diff --git a/src/Plugins/Freetype/tt_tools.cpp b/src/Plugins/Freetype/tt_tools.cpp index 813078036..74f4bafa3 100644 --- a/src/Plugins/Freetype/tt_tools.cpp +++ b/src/Plugins/Freetype/tt_tools.cpp @@ -12,6 +12,7 @@ #include "tt_tools.hpp" #include "analyze.hpp" #include "file.hpp" +#include "iterator.hpp" #include "tm_file.hpp" #include "tree_helper.hpp" #include "tt_file.hpp" @@ -372,6 +373,90 @@ tt_font_name (url u) { * OpenType MATH table ******************************************************************************/ +unsigned int +ot_mathtable_rep::get_init_glyphID (unsigned int glyphID) { + // init cache + if (N (get_init_glyphID_cache) == 0) { + auto it= iterate (ver_glyph_variants); + while (it->busy ()) { + unsigned int gid= it->next (); + array v = ver_glyph_variants (gid); + for (unsigned vid : v) { + get_init_glyphID_cache (vid)= gid; + } + } + it= iterate (hor_glyph_variants); + while (it->busy ()) { + unsigned int gid= it->next (); + array v = hor_glyph_variants (gid); + for (unsigned vid : v) { + get_init_glyphID_cache (vid)= gid; + } + } + } + + // look up cache + if (get_init_glyphID_cache->contains (glyphID)) { + return get_init_glyphID_cache (glyphID); + } + + return glyphID; +} + +bool +MathKernInfoRecord::has_kerning (bool top, bool left) { + return (top ? (left ? hasTopLeft : hasTopRight) + : (left ? hasBottomLeft : hasBottomRight)); +} + +// get the kerning value of a glyphID with a given height +// should be called after has_kerning +int +MathKernInfoRecord::get_kerning (int height, bool top, bool left) { + MathKernTable& kt= + top ? (left ? topLeft : topRight) : (left ? bottomLeft : bottomRight); + + int idx= 0, n= (int) kt.heightCount; + + // only one kerning value + if (n == 0) { + return kt.kernValues[0]; + } + + // find the kerning value + if (height < kt.correctionHeight[0]) { + idx= 0; // below the first height entry + } + else if (height >= kt.correctionHeight[n - 1]) { + idx= n; // above the last height entry + } + else { + for (idx= 1; idx <= n - 1; idx++) { + if (height >= kt.correctionHeight[idx - 1] && + height < kt.correctionHeight[idx]) { + break; + } + } + } + // cout << "get_kerning : height " << height << " -> " << idx << " -> " + // << kt.kernValues[idx] << LF; + return kt.kernValues[idx]; +} + +bool +ot_mathtable_rep::has_kerning (unsigned int glyphID, bool top, bool left) { + return math_kern_info->contains (glyphID) && + math_kern_info (glyphID).has_kerning (top, left); +} + +int +ot_mathtable_rep::get_kerning (unsigned int glyphID, int height, bool top, + bool left) { + // should be called after has_kerning + auto& record= math_kern_info (glyphID); + return record.get_kerning (height, top, left); +} + // a helper function to parse the coverage table // return an array of glyphID static array diff --git a/src/Plugins/Freetype/tt_tools.hpp b/src/Plugins/Freetype/tt_tools.hpp index 4d7f612f2..b7e40a221 100644 --- a/src/Plugins/Freetype/tt_tools.hpp +++ b/src/Plugins/Freetype/tt_tools.hpp @@ -13,6 +13,7 @@ #define TT_TOOLS_H #include "basic.hpp" +#include "hashmap.hpp" #include "hashset.hpp" #include "tm_debug.hpp" #include "tree.hpp" @@ -215,6 +216,9 @@ struct MathKernInfoRecord { MathKernInfoRecord () : hasTopRight (false), hasTopLeft (false), hasBottomRight (false), hasBottomLeft (false) {} + + bool has_kerning (bool top, bool left); + int get_kerning (int height, bool top, bool left); }; struct GlyphPartRecord { @@ -247,6 +251,14 @@ struct ot_mathtable_rep : concrete_struct { hashmap> hor_glyph_variants_adv; hashmap ver_glyph_assembly; hashmap hor_glyph_assembly; + + // helper functions and data + hashmap get_init_glyphID_cache; + // for variant glyph, get the glyphID of the base glyph + unsigned int get_init_glyphID (unsigned int glyphID); + + bool has_kerning (unsigned int glyphID, bool top, bool left); + int get_kerning (unsigned int glyphID, int height, bool top, bool left); }; struct ot_mathtable { diff --git a/src/Plugins/Freetype/unicode_font.cpp b/src/Plugins/Freetype/unicode_font.cpp index 23f5dbb17..1ec3e6e16 100644 --- a/src/Plugins/Freetype/unicode_font.cpp +++ b/src/Plugins/Freetype/unicode_font.cpp @@ -10,6 +10,9 @@ ******************************************************************************/ #include "unicode_font.hpp" +#include "analyze.hpp" +#include "font.hpp" +#include "scalable.hpp" #include #include @@ -908,6 +911,10 @@ unicode_font_rep::get_left_correction (string s) { SI unicode_font_rep::get_right_correction (string s) { + if (math_type == MATH_TYPE_OPENTYPE) { + SI r= 0; + if (get_ot_italic_correction (s, r)) return r; + } metric ex; get_extents (s, ex); if (math_type == MATH_TYPE_TEX_GYRE && is_integral (s)) @@ -918,6 +925,10 @@ unicode_font_rep::get_right_correction (string s) { SI unicode_font_rep::get_lsub_correction (string s) { + if (math_type == MATH_TYPE_OPENTYPE) { + SI r= 0; + if (get_ot_kerning (s, y1, false, true, r)) return r; + } SI r= -get_left_correction (s) + global_lsub_correct; if (math_type == MATH_TYPE_STIX && (is_integral (s) || is_alt_integral (s))) ; @@ -929,6 +940,10 @@ unicode_font_rep::get_lsub_correction (string s) { SI unicode_font_rep::get_lsup_correction (string s) { + if (math_type == MATH_TYPE_OPENTYPE) { + SI r= 0; + if (get_ot_kerning (s, y2, true, true, r)) return r; + } SI r= global_lsup_correct; if (math_type == MATH_TYPE_STIX && (is_integral (s) || is_alt_integral (s))) r+= get_right_correction (s); @@ -940,6 +955,19 @@ unicode_font_rep::get_lsup_correction (string s) { SI unicode_font_rep::get_rsub_correction (string s) { + if (math_type == MATH_TYPE_OPENTYPE) { + SI ic= 0, kern= 0; + bool has_ic = get_ot_italic_correction (s, ic); + bool has_kern= get_ot_kerning (s, y1, false, false, kern); + + if (has_ic || has_kern) { + // for integral, we use 3/5 of italic correction for rsub, otherwise 0 + ic = is_ot_integral (s) ? (SI) (0.6 * ic) : 0; + SI r= -ic + kern; + // cout << "get_rsup_correction for: " << s << " " << rr << LF; + return r; + } + } SI r= global_rsub_correct; if (math_type == MATH_TYPE_STIX && (is_integral (s) || is_alt_integral (s))) ; @@ -954,6 +982,19 @@ SI unicode_font_rep::get_rsup_correction (string s) { // cout << "Check " << s << ", " << rsup_correct[s] << ", " << this->res_name // << LF; + if (math_type == MATH_TYPE_OPENTYPE) { + SI ic= 0, kern= 0; + bool has_ic = get_ot_italic_correction (s, ic); + bool has_kern= get_ot_kerning (s, y2, true, false, kern); + + if (has_ic || has_kern) { + // for integral signs, we use 2/5 of italic correction for rsup + if (is_ot_integral (s)) ic= (SI) (0.4 * ic); + SI r= ic + kern; + // cout << "get_rsup_correction for: " << s << " " << rr << LF; + return r; + } + } SI r= get_right_correction (s) + global_rsup_correct; if (math_type == MATH_TYPE_STIX && (is_integral (s) || is_alt_integral (s))) ; @@ -1011,12 +1052,12 @@ unicode_font_rep::init_design_unit_factor () { inline SI unicode_font_rep::design_unit_to_metric (int du) { - return (SI) design_unit_to_metric_factor * du; + return (SI) (design_unit_to_metric_factor * du); } inline int unicode_font_rep::metric_to_design_unit (SI m) { - return (int) metric_to_design_unit_factor * m; + return (int) (metric_to_design_unit_factor * m); } font @@ -1027,6 +1068,100 @@ unicode_font_rep::make_rubber_font (font base) { return font_rep::make_rubber_font (base); } +inline int +decode_index (FT_Face face, int i) { + if (i < 0xc000000) return ft_get_char_index (face, i); + return i - 0xc000000; +} + +inline unsigned int +unicode_font_rep::get_glyphID (string s) { + // <@XXXX> + if (starts (s, "<@")) { + return from_hex (s (2, 6)); + } + font_metric fm; + font_glyphs fg; + int index= index_glyph (s, fm, fg); + return decode_index (math_face->ft_face, index); +} + +inline string +get_left (string s) { + if (N (s) == 0) return s; + int i= 0; + tm_char_forwards (s, i); + return s (0, i); +} + +inline string +get_right (string s) { + if (N (s) == 0) return s; + int i= N (s); + tm_char_backwards (s, i); + return s (i, N (s)); +} + +bool +unicode_font_rep::get_ot_italic_correction (string s, SI& r) { + if (math_type != MATH_TYPE_OPENTYPE || N (s) == 0) return false; + + auto italics_correction= math_table->italics_correction; + // italic correction is only available for right side of the glyph + string ss = get_right (s); + unsigned int glyphID= get_glyphID (ss); + + if (italics_correction->contains (glyphID)) { + int correction= italics_correction[glyphID].value; + r = design_unit_to_metric (correction); + return true; + } + return false; +} + +bool +unicode_font_rep::get_ot_kerning (string s, SI height, bool top, bool left, + SI& kerning) { + if (math_type != MATH_TYPE_OPENTYPE || N (s) == 0) return false; + + string ss = left ? get_left (s) : get_right (s); + unsigned int glyphID= get_glyphID (ss); + + if (!math_table->has_kerning (glyphID, top, left)) return false; + + int kerning_unit= math_table->get_kerning ( + glyphID, metric_to_design_unit (height), top, left); + + kerning= design_unit_to_metric (kerning_unit); + // cout << "Kerning for " << ss << " with height: " << kerning_unit << " -> " + // << kerning << LF; + return true; +} + +bool +unicode_font_rep::is_ot_integral (string s) { + if (math_type != MATH_TYPE_OPENTYPE) return false; + if (N (ot_integral) == 0) { + array integrals; + integrals << string ("") << string ("") << string ("") + << string ("") << string ("") << string ("") + << string ("") << string ("") + << string ("") << string ("") + << string ("") << string ("") + << string ("") << string ("") + << string ("") << string ("") + << string ("") << string ("") + << string ("") << string (""); + for (auto integral : integrals) { + ot_integral << get_glyphID (integral); + } + } + unsigned int glyphID= get_glyphID (s); + // if variant, we turn to the base glyphID + glyphID= math_table->get_init_glyphID (glyphID); + return ot_integral->contains (glyphID); +} + font unicode_font (string family, int size, int dpi) { return unicode_font (family, size, dpi, dpi); diff --git a/src/Plugins/Freetype/unicode_font.hpp b/src/Plugins/Freetype/unicode_font.hpp index b367ab70c..b0a4478bc 100644 --- a/src/Plugins/Freetype/unicode_font.hpp +++ b/src/Plugins/Freetype/unicode_font.hpp @@ -36,10 +36,15 @@ struct unicode_font_rep : font_rep { tt_face math_face; ot_mathtable math_table; font make_rubber_font (font base) override; + bool get_ot_kerning (string s, SI height, bool top, bool left, SI& kerning); + bool get_ot_italic_correction (string s, SI& r); + bool is_ot_integral (string s); + hashset ot_integral; unicode_font_rep (string name, string family, int size, int hdpi, int vdpi); void tex_gyre_operators (); + unsigned int get_glyphID (string s); unsigned int read_unicode_char (string s, int& i); unsigned int ligature_replace (unsigned int c, string s, int& i); bool supports (string c);