Last active
November 1, 2024 10:45
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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