Skip to content

Commit

Permalink
[80_3] Enhance sup/subscript kerning via OpenType math table (#2090)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
KeShih authored Oct 29, 2024
1 parent 4d6a517 commit f31dc6c
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 2 deletions.
63 changes: 63 additions & 0 deletions devel/80_3.tmu
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<TMU|<tuple|1.0.5|1.2.9.5>>

<style|generic>

<\body>
Asana Math:

<math|<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>>

<\equation*>
<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>
</equation*>

\;

Fira Math:

<\with|font|Fira Math>
<math|<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>>

<\equation*>
<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>
</equation*>
</with>

\;

TeX Gyre Pagella Math:

<\with|font|TeX Gyre Pagella Math>
<math|<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>>

<\equation*>
<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>
</equation*>

\;
</with>

TeX Gyre Schola Math:

<\with|font|TeX Gyre Schola Math>
<math|<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>>

<\equation*>
<big|oint><rsub|1><rsup|1><big|int><rsub|1><rsup|1><big|iint><rsub|1><rsup|1>𝑒<rsup|𝑥>dx BA<rsub|1><rsup|1> UV<rsub|1><rsup|1>ZZ<rsub|1><rsup|1>PQ<rsub|1><rsup|1>\<Omega\><rsub|1><rsup|1>\<Lambda\><rsub|1><rsup|1>\<Delta\><rsub|4><rsup|4>\<alpha\><rsub|4><rsup|4>\<b-cal-M\><rsub|4><rsup|4>𝓐<rsub|4><rsup|4>𝓥<rsup|4><rsub|4>𝓦<rsub|4><rsup|4>
</equation*>
</with>

\;

\;
</body>

<\initial>
<\collection>
<associate|font|Asana Math>
<associate|font-base-size|14>
<associate|font-family|rm>
<associate|page-medium|paper>
<associate|page-screen-margin|false>
</collection>
</initial>
85 changes: 85 additions & 0 deletions src/Plugins/Freetype/tt_tools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<unsigned int> 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<unsigned int> 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<unsigned int>
Expand Down
12 changes: 12 additions & 0 deletions src/Plugins/Freetype/tt_tools.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#define TT_TOOLS_H

#include "basic.hpp"
#include "hashmap.hpp"
#include "hashset.hpp"
#include "tm_debug.hpp"
#include "tree.hpp"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -247,6 +251,14 @@ struct ot_mathtable_rep : concrete_struct {
hashmap<unsigned int, array<unsigned int>> hor_glyph_variants_adv;
hashmap<unsigned int, GlyphAssembly> ver_glyph_assembly;
hashmap<unsigned int, GlyphAssembly> hor_glyph_assembly;

// helper functions and data
hashmap<unsigned int, unsigned int> 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 {
Expand Down
139 changes: 137 additions & 2 deletions src/Plugins/Freetype/unicode_font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
******************************************************************************/

#include "unicode_font.hpp"
#include "analyze.hpp"
#include "font.hpp"
#include "scalable.hpp"
#include <lolly/data/numeral.hpp>
#include <lolly/data/unicode.hpp>

Expand Down Expand Up @@ -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))
Expand All @@ -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)))
;
Expand All @@ -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);
Expand All @@ -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)))
;
Expand All @@ -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)))
;
Expand Down Expand Up @@ -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
Expand All @@ -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<string> integrals;
integrals << string ("<int>") << string ("<iiint>") << string ("<iiiint>")
<< string ("<oint>") << string ("<oiint>") << string ("<oiiint>")
<< string ("<upint>") << string ("<upiint>")
<< string ("<upiiint>") << string ("<upiiiint>")
<< string ("<upoint>") << string ("<upoiint>")
<< string ("<upoiiint>") << string ("<intlim>")
<< string ("<iintlim>") << string ("<iiintlim>")
<< string ("<iiiintlim>") << string ("<ointlim>")
<< string ("<oiintlim>") << string ("<oiiintlim>");
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);
Expand Down
Loading

0 comments on commit f31dc6c

Please sign in to comment.