-
-
Save madig/76567a9650de639bbff51ce010783790 to your computer and use it in GitHub Desktop.
Align kerning groups and kerning
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
# pyright: basic | |
from __future__ import annotations | |
import argparse | |
import logging | |
from pathlib import Path | |
from fontTools.designspaceLib import DesignSpaceDocument | |
from fontTools.ufoLib.kerning import lookupKerningValue | |
from ufoLib2 import Font | |
parser = argparse.ArgumentParser() | |
parser.add_argument("designspace", type=Path, help="The source Designspace.") | |
args = parser.parse_args() | |
designspace_path: Path = args.designspace | |
designspace = DesignSpaceDocument.fromfile(designspace_path) | |
designspace.loadSourceFonts(Font.open) | |
# Step 1: Align kerning groups. Some fonts group the same glyphs under different names | |
# (e.g. some source revisions of Roboto Serif). Rename them to what the default source | |
# calls them, and discard groups that regroup glyphs or don't appear in the default | |
# source. | |
default_source: Font = designspace.findDefault().font | |
canonical_group_name_kern1: dict[tuple[str, ...], str] = { | |
tuple(sorted(members)): group_name | |
for group_name, members in default_source.groups.items() | |
if group_name.startswith("public.kern1.") | |
} | |
canonical_group_name_kern2: dict[tuple[str, ...], str] = { | |
tuple(sorted(members)): group_name | |
for group_name, members in default_source.groups.items() | |
if group_name.startswith("public.kern2.") | |
} | |
for source in designspace.sources: | |
if source.layerName is not None: | |
continue | |
assert source.font is not None | |
font: Font = source.font | |
renamed_groups: dict[str, str] = {} | |
deleted_groups: list[str] = [] | |
overwritten_groups: list[str] = [] | |
for group_name, group_members in font.groups.items(): | |
if not group_members: | |
continue | |
if group_name.startswith("public.kern1."): | |
is_kern1 = True | |
elif group_name.startswith("public.kern2."): | |
is_kern1 = False | |
else: | |
continue | |
member_key = tuple(sorted(group_members)) | |
if is_kern1: | |
canonical_name = canonical_group_name_kern1.get(member_key) | |
else: | |
canonical_name = canonical_group_name_kern2.get(member_key) | |
if canonical_name is None: | |
if group_name in default_source.groups: | |
# This group appears in the default source, but with different content. | |
logging.warning( | |
"Overwriting group %s in %s with group from the default " | |
"source because the content differs", | |
group_name, | |
source.name, | |
) | |
overwritten_groups.append(group_name) | |
else: | |
# This group doesn't appear in the default source. Discard it. | |
logging.warning( | |
"Discarding group %s in %s because it does not appear in the " | |
"default source", | |
group_name, | |
source.name, | |
) | |
deleted_groups.append(group_name) | |
elif group_name != canonical_name: | |
# This group appears in the default source, but under a different name. | |
# Rename it. | |
logging.warning( | |
"Renaming group %s in %s to %s to match the default source", | |
group_name, | |
source.name, | |
canonical_name, | |
) | |
# deleted_groups.append(group_name) | |
# font.groups[canonical_name] = group_members | |
renamed_groups[group_name] = canonical_name | |
for group_name in deleted_groups: | |
del font.groups[group_name] | |
for group_name in overwritten_groups: | |
font.groups[group_name] = list(default_source.groups[group_name]) | |
# Rename kerning pairs to use the new canonical names, if any. | |
if renamed_groups: | |
for old_name, new_name in renamed_groups.items(): | |
# TODO: Handle edge cases like swapped names? They'd crash at least by | |
# popping a non-existent key? | |
members = font.groups.pop(old_name) | |
font.groups[new_name] = members | |
new_kerning = {} | |
for (first, second), value in font.kerning.items(): | |
new_kerning[ | |
(renamed_groups.get(first, first), renamed_groups.get(second, second)) | |
] = value | |
font.kerning = new_kerning | |
assert not font.groups.keys() & deleted_groups | |
assert not any( | |
side in renamed_groups for pair in font.kerning.keys() for side in pair | |
) | |
assert all( | |
font.groups[group_name] == default_source.groups[group_name] | |
for group_name in overwritten_groups | |
) | |
# Step 2: Take the union of all kerning pairs, then for each source, fill in missing | |
# pairs as the UFO kerning lookup algorithm would. This ensures that kerning exceptions | |
# are handled correctly when they are present in one source (used as is) but not another | |
# (backfilled by the less specific pair or falling back to zero). | |
union_kerning: set[tuple[str, str]] = { | |
pair for source in designspace.sources for pair in source.font.kerning | |
} | |
glyph_to_first_group = { | |
glyph_name: group_name | |
for glyph_names, group_name in canonical_group_name_kern1.items() | |
for glyph_name in glyph_names | |
} | |
glyph_to_second_group = { | |
glyph_name: group_name | |
for glyph_names, group_name in canonical_group_name_kern2.items() | |
for glyph_name in glyph_names | |
} | |
for source in designspace.sources: | |
if source.layerName is not None: | |
continue | |
font: Font = source.font | |
missing_kerning_pairs = union_kerning - font.kerning.keys() | |
for pair in missing_kerning_pairs: | |
value = lookupKerningValue( | |
pair, | |
font.kerning, | |
font.groups, | |
glyphToFirstGroup=glyph_to_first_group, | |
glyphToSecondGroup=glyph_to_second_group, | |
) | |
font.kerning[pair] = value | |
assert font.kerning.keys() == union_kerning | |
font.save() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment