Skip to content

Instantly share code, notes, and snippets.

@typoman
Last active November 1, 2024 10:45
Show Gist options
  • Save typoman/ad934621d3e8d7b7b27f472dc29763e1 to your computer and use it in GitHub Desktop.
Save typoman/ad934621d3e8d7b7b27f472dc29763e1 to your computer and use it in GitHub Desktop.
To fix the issue where fontmake skips kerning pairs: "Skipping kerning pair with mixed direction (LTR, RTL)" by splitting mixed RTL/Neutral kerning groups in a font and adding new kerning if necessary.
from fontgadgets.extensions.groups.kerning_groups import isKerningGroup
from ufo2ft.featureWriters import KernFeatureWriter
from ufo2ft.featureWriters.kernFeatureWriter import COMMON_SCRIPTS_SET, DFLT_SCRIPTS, script_direction
from ufo2ft.featureCompiler import parseLayoutFeatures
"""
RoboFont Script
- Type: Mastering
- Purpose: To fix the issue where fontmake skips kerning pairs: "Skipping kerning pair with mixed direction (LTR, RTL)" by splitting mixed RTL/Neutral kerning groups in a font and adding new kerning if necessary.
- Specifications:
- Parse the open type features of a font to determine the direction of each glyph
- Split mixed RTL/Neutral kerning groups into separate groups
- Add new kerning pairs to for the newly added groups
- Publish Date: 2024-10-31
- Author: Bahman Eslami
- License: MIT
"""
def getGlyphsDirectionsMap(f):
"""
Creates a mapping of glyph names to their corresponding directions.
This function takes a defcon font obj as input, parses its open type
features, and determines the direction of each glyph based on the scripts
it belongs to.
Args:
f (defcon.Font object)
Returns:
dict: A dictionary mapping glyph names to their directions (LTR, RTL, or Auto).
Raises:
ValueError: If a glyph has more than one direction.
"""
writer = KernFeatureWriter()
feaFile = parseLayoutFeatures(f)
ctx = writer.setContext(f, feaFile)
glyphScripts = ctx.glyphScripts
direction_map = {}
for g in f:
gn = g.name
scripts = glyphScripts.get(gn, DFLT_SCRIPTS)
if scripts & DFLT_SCRIPTS:
scripts = COMMON_SCRIPTS_SET
for s in scripts:
direction_map.setdefault(gn, set()).add(script_direction(s))
if len(direction_map[gn]) > 1:
raise ValueError(f"More than one direction for glyph `{gn}` !")
direction_map[gn] = direction_map[gn].pop()
return direction_map
def splitKerningGroupsByDirection(f):
"""
Splits mixed RTL/Neutral kerning groups in a font and adds new kerning if necessary.
This function takes a font object as input and modifies its kerning groups and pairs.
It checks each kerning group for mixed RTL/Neutral glyphs and splits them into separate groups.
New kerning pairs are added to prevent LTR/RTL pairs.
Args:
f (defcon.Font object)
Returns:
None
Raises:
ValueError: If a kerning group contains glyphs with more than two different directions.
"""
skipExportGlyphs = set(f.lib.get("public.skipExportGlyphs", [])) | set(["space", ".notdef", ".fallbackGlyph"])
newGroups = {}
groups = dict(f.groups)
kerning = dict(f.kerning)
old_to_new = {}
direction_map = getGlyphsDirectionsMap(f)
for group, members in groups.items():
if not isKerningGroup(group):
continue
divide_by_direction = {} # group glyphs based on the direction
for gname in members:
direction = direction_map[gname]
divide_by_direction.setdefault(direction, set()).add(gname)
if len(divide_by_direction) > 2:
raise ValueError(f'Mixed direction glyphs inside group:\n{group}\n{members}')
elif len(divide_by_direction) == 2:
if "LTR" in divide_by_direction:
continue
for direction, glyphs in divide_by_direction.items():
# Group with both "RTL" and "Auto" glyph directions now
# splitting, which is the main cause of the error.
newMembers = list(sorted(set(glyphs) - skipExportGlyphs, key=lambda g: members.index(g)))
newGroupName = group
if direction != "Auto":
newGroupName = f"{group}_{direction}"
newGroups[newGroupName] = newMembers
old_to_new.setdefault(group, {})[direction] = newGroupName
direction_map[newGroupName] = direction
else:
direction = next(iter(divide_by_direction.keys()))
direction_map[group] = direction
newKerning = {}
# Duplicate the existing kerning to the newly splitted groups
for kp, kv in kerning.items():
entries_to_split = set(kp) & newGroups.keys()
if entries_to_split:
translator = {}
for e in entries_to_split:
for direction, new_name in old_to_new[e].items():
if direction != "Auto":
translator[e] = new_name
new_pair = [translator.get(e, e) for e in kp]
newKerning[tuple(new_pair)] = kv
f.groups.update(newGroups)
f.kerning.update(newKerning)
if __name__ == '__main__':
for f in AllFonts():
f = f.naked()
splitKerningGroupsByDirection(f)
# f.save()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment