Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| from typing import Mapping, Any | |
| from collections.abc import Container | |
| from fontTools.annotations import KerningNested | |
| """ | |
| Functions for converting UFO1 or UFO2 files into UFO3 format. | |
| Currently provides functionality for converting kerning rules | |
| and kerning groups. Conversion is only supported _from_ UFO1 | |
| or UFO2, and _to_ UFO3. | |
| """ | |
| # adapted from the UFO spec | |
| def convertUFO1OrUFO2KerningToUFO3Kerning( | |
| kerning: KerningNested, groups: dict[str, list[str]], glyphSet: Container[str] = () | |
| ) -> tuple[KerningNested, dict[str, list[str]], dict[str, dict[str, str]]]: | |
| """Convert kerning data in UFO1 or UFO2 syntax into UFO3 syntax. | |
| Args: | |
| kerning: | |
| A dictionary containing the kerning rules defined in | |
| the UFO font, as used in :class:`.UFOReader` objects. | |
| groups: | |
| A dictionary containing the groups defined in the UFO | |
| font, as used in :class:`.UFOReader` objects. | |
| glyphSet: | |
| Optional; a set of glyph objects to skip (default: None). | |
| Returns: | |
| 1. A dictionary representing the converted kerning data. | |
| 2. A copy of the groups dictionary, with all groups renamed to UFO3 syntax. | |
| 3. A dictionary containing the mapping of old group names to new group names. | |
| """ | |
| # gather known kerning groups based on the prefixes | |
| firstReferencedGroups, secondReferencedGroups = findKnownKerningGroups(groups) | |
| # Make lists of groups referenced in kerning pairs. | |
| for first, seconds in list(kerning.items()): | |
| if first in groups and first not in glyphSet: | |
| if not first.startswith("public.kern1."): | |
| firstReferencedGroups.add(first) | |
| for second in list(seconds.keys()): | |
| if second in groups and second not in glyphSet: | |
| if not second.startswith("public.kern2."): | |
| secondReferencedGroups.add(second) | |
| # Create new names for these groups. | |
| firstRenamedGroups: dict[str, str] = {} | |
| for first in firstReferencedGroups: | |
| # Make a list of existing group names. | |
| existingGroupNames = list(groups.keys()) + list(firstRenamedGroups.keys()) | |
| # Remove the old prefix from the name | |
| newName = first.replace("@MMK_L_", "") | |
| # Add the new prefix to the name. | |
| newName = "public.kern1." + newName | |
| # Make a unique group name. | |
| newName = makeUniqueGroupName(newName, existingGroupNames) | |
| # Store for use later. | |
| firstRenamedGroups[first] = newName | |
| secondRenamedGroups: dict[str, str] = {} | |
| for second in secondReferencedGroups: | |
| # Make a list of existing group names. | |
| existingGroupNames = list(groups.keys()) + list(secondRenamedGroups.keys()) | |
| # Remove the old prefix from the name | |
| newName = second.replace("@MMK_R_", "") | |
| # Add the new prefix to the name. | |
| newName = "public.kern2." + newName | |
| # Make a unique group name. | |
| newName = makeUniqueGroupName(newName, existingGroupNames) | |
| # Store for use later. | |
| secondRenamedGroups[second] = newName | |
| # Populate the new group names into the kerning dictionary as needed. | |
| newKerning = {} | |
| for first, seconds in list(kerning.items()): | |
| first = firstRenamedGroups.get(first, first) | |
| newSeconds = {} | |
| for second, value in list(seconds.items()): | |
| second = secondRenamedGroups.get(second, second) | |
| newSeconds[second] = value | |
| newKerning[first] = newSeconds | |
| # Make copies of the referenced groups and store them | |
| # under the new names in the overall groups dictionary. | |
| allRenamedGroups = list(firstRenamedGroups.items()) | |
| allRenamedGroups += list(secondRenamedGroups.items()) | |
| for oldName, newName in allRenamedGroups: | |
| group = list(groups[oldName]) | |
| groups[newName] = group | |
| # Return the kerning and the groups. | |
| return newKerning, groups, dict(side1=firstRenamedGroups, side2=secondRenamedGroups) | |
| def findKnownKerningGroups(groups: Mapping[str, Any]) -> tuple[set[str], set[str]]: | |
| """Find all kerning groups in a UFO1 or UFO2 font that use known prefixes. | |
| In some cases, not all kerning groups will be referenced | |
| by the kerning pairs in a UFO. The algorithm for locating | |
| groups in :func:`convertUFO1OrUFO2KerningToUFO3Kerning` will | |
| miss these unreferenced groups. By scanning for known prefixes, | |
| this function will catch all of the prefixed groups. | |
| The prefixes and sides by this function are: | |
| @MMK_L_ - side 1 | |
| @MMK_R_ - side 2 | |
| as defined in the UFO1 specification. | |
| Args: | |
| groups: | |
| A dictionary containing the groups defined in the UFO | |
| font, as read by :class:`.UFOReader`. | |
| Returns: | |
| Two sets; the first containing the names of all | |
| first-side kerning groups identified in the ``groups`` | |
| dictionary, and the second containing the names of all | |
| second-side kerning groups identified. | |
| "First-side" and "second-side" are with respect to the | |
| writing direction of the script. | |
| Example:: | |
| >>> testGroups = { | |
| ... "@MMK_L_1" : None, | |
| ... "@MMK_L_2" : None, | |
| ... "@MMK_L_3" : None, | |
| ... "@MMK_R_1" : None, | |
| ... "@MMK_R_2" : None, | |
| ... "@MMK_R_3" : None, | |
| ... "@MMK_l_1" : None, | |
| ... "@MMK_r_1" : None, | |
| ... "@MMK_X_1" : None, | |
| ... "foo" : None, | |
| ... } | |
| >>> first, second = findKnownKerningGroups(testGroups) | |
| >>> sorted(first) == ['@MMK_L_1', '@MMK_L_2', '@MMK_L_3'] | |
| True | |
| >>> sorted(second) == ['@MMK_R_1', '@MMK_R_2', '@MMK_R_3'] | |
| True | |
| """ | |
| knownFirstGroupPrefixes = ["@MMK_L_"] | |
| knownSecondGroupPrefixes = ["@MMK_R_"] | |
| firstGroups = set() | |
| secondGroups = set() | |
| for groupName in list(groups.keys()): | |
| for firstPrefix in knownFirstGroupPrefixes: | |
| if groupName.startswith(firstPrefix): | |
| firstGroups.add(groupName) | |
| break | |
| for secondPrefix in knownSecondGroupPrefixes: | |
| if groupName.startswith(secondPrefix): | |
| secondGroups.add(groupName) | |
| break | |
| return firstGroups, secondGroups | |
| def makeUniqueGroupName(name: str, groupNames: list[str], counter: int = 0) -> str: | |
| """Make a kerning group name that will be unique within the set of group names. | |
| If the requested kerning group name already exists within the set, this | |
| will return a new name by adding an incremented counter to the end | |
| of the requested name. | |
| Args: | |
| name: | |
| The requested kerning group name. | |
| groupNames: | |
| A list of the existing kerning group names. | |
| counter: | |
| Optional; a counter of group names already seen (default: 0). If | |
| :attr:`.counter` is not provided, the function will recurse, | |
| incrementing the value of :attr:`.counter` until it finds the | |
| first unused ``name+counter`` combination, and return that result. | |
| Returns: | |
| A unique kerning group name composed of the requested name suffixed | |
| by the smallest available integer counter. | |
| """ | |
| # Add a number to the name if the counter is higher than zero. | |
| newName = name | |
| if counter > 0: | |
| newName = "%s%d" % (newName, counter) | |
| # If the new name is in the existing group names, recurse. | |
| if newName in groupNames: | |
| return makeUniqueGroupName(name, groupNames, counter + 1) | |
| # Otherwise send back the new name. | |
| return newName | |
| def test(): | |
| """ | |
| Tests for :func:`.convertUFO1OrUFO2KerningToUFO3Kerning`. | |
| No known prefixes. | |
| >>> testKerning = { | |
| ... "A" : { | |
| ... "A" : 1, | |
| ... "B" : 2, | |
| ... "CGroup" : 3, | |
| ... "DGroup" : 4 | |
| ... }, | |
| ... "BGroup" : { | |
| ... "A" : 5, | |
| ... "B" : 6, | |
| ... "CGroup" : 7, | |
| ... "DGroup" : 8 | |
| ... }, | |
| ... "CGroup" : { | |
| ... "A" : 9, | |
| ... "B" : 10, | |
| ... "CGroup" : 11, | |
| ... "DGroup" : 12 | |
| ... }, | |
| ... } | |
| >>> testGroups = { | |
| ... "BGroup" : ["B"], | |
| ... "CGroup" : ["C"], | |
| ... "DGroup" : ["D"], | |
| ... } | |
| >>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning( | |
| ... testKerning, testGroups, []) | |
| >>> expected = { | |
| ... "A" : { | |
| ... "A": 1, | |
| ... "B": 2, | |
| ... "public.kern2.CGroup": 3, | |
| ... "public.kern2.DGroup": 4 | |
| ... }, | |
| ... "public.kern1.BGroup": { | |
| ... "A": 5, | |
| ... "B": 6, | |
| ... "public.kern2.CGroup": 7, | |
| ... "public.kern2.DGroup": 8 | |
| ... }, | |
| ... "public.kern1.CGroup": { | |
| ... "A": 9, | |
| ... "B": 10, | |
| ... "public.kern2.CGroup": 11, | |
| ... "public.kern2.DGroup": 12 | |
| ... } | |
| ... } | |
| >>> kerning == expected | |
| True | |
| >>> expected = { | |
| ... "BGroup": ["B"], | |
| ... "CGroup": ["C"], | |
| ... "DGroup": ["D"], | |
| ... "public.kern1.BGroup": ["B"], | |
| ... "public.kern1.CGroup": ["C"], | |
| ... "public.kern2.CGroup": ["C"], | |
| ... "public.kern2.DGroup": ["D"], | |
| ... } | |
| >>> groups == expected | |
| True | |
| Known prefixes. | |
| >>> testKerning = { | |
| ... "A" : { | |
| ... "A" : 1, | |
| ... "B" : 2, | |
| ... "@MMK_R_CGroup" : 3, | |
| ... "@MMK_R_DGroup" : 4 | |
| ... }, | |
| ... "@MMK_L_BGroup" : { | |
| ... "A" : 5, | |
| ... "B" : 6, | |
| ... "@MMK_R_CGroup" : 7, | |
| ... "@MMK_R_DGroup" : 8 | |
| ... }, | |
| ... "@MMK_L_CGroup" : { | |
| ... "A" : 9, | |
| ... "B" : 10, | |
| ... "@MMK_R_CGroup" : 11, | |
| ... "@MMK_R_DGroup" : 12 | |
| ... }, | |
| ... } | |
| >>> testGroups = { | |
| ... "@MMK_L_BGroup" : ["B"], | |
| ... "@MMK_L_CGroup" : ["C"], | |
| ... "@MMK_L_XGroup" : ["X"], | |
| ... "@MMK_R_CGroup" : ["C"], | |
| ... "@MMK_R_DGroup" : ["D"], | |
| ... "@MMK_R_XGroup" : ["X"], | |
| ... } | |
| >>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning( | |
| ... testKerning, testGroups, []) | |
| >>> expected = { | |
| ... "A" : { | |
| ... "A": 1, | |
| ... "B": 2, | |
| ... "public.kern2.CGroup": 3, | |
| ... "public.kern2.DGroup": 4 | |
| ... }, | |
| ... "public.kern1.BGroup": { | |
| ... "A": 5, | |
| ... "B": 6, | |
| ... "public.kern2.CGroup": 7, | |
| ... "public.kern2.DGroup": 8 | |
| ... }, | |
| ... "public.kern1.CGroup": { | |
| ... "A": 9, | |
| ... "B": 10, | |
| ... "public.kern2.CGroup": 11, | |
| ... "public.kern2.DGroup": 12 | |
| ... } | |
| ... } | |
| >>> kerning == expected | |
| True | |
| >>> expected = { | |
| ... "@MMK_L_BGroup": ["B"], | |
| ... "@MMK_L_CGroup": ["C"], | |
| ... "@MMK_L_XGroup": ["X"], | |
| ... "@MMK_R_CGroup": ["C"], | |
| ... "@MMK_R_DGroup": ["D"], | |
| ... "@MMK_R_XGroup": ["X"], | |
| ... "public.kern1.BGroup": ["B"], | |
| ... "public.kern1.CGroup": ["C"], | |
| ... "public.kern1.XGroup": ["X"], | |
| ... "public.kern2.CGroup": ["C"], | |
| ... "public.kern2.DGroup": ["D"], | |
| ... "public.kern2.XGroup": ["X"], | |
| ... } | |
| >>> groups == expected | |
| True | |
| >>> from .validators import kerningValidator | |
| >>> kerningValidator(kerning) | |
| (True, None) | |
| Mixture of known prefixes and groups without prefixes. | |
| >>> testKerning = { | |
| ... "A" : { | |
| ... "A" : 1, | |
| ... "B" : 2, | |
| ... "@MMK_R_CGroup" : 3, | |
| ... "DGroup" : 4 | |
| ... }, | |
| ... "BGroup" : { | |
| ... "A" : 5, | |
| ... "B" : 6, | |
| ... "@MMK_R_CGroup" : 7, | |
| ... "DGroup" : 8 | |
| ... }, | |
| ... "@MMK_L_CGroup" : { | |
| ... "A" : 9, | |
| ... "B" : 10, | |
| ... "@MMK_R_CGroup" : 11, | |
| ... "DGroup" : 12 | |
| ... }, | |
| ... } | |
| >>> testGroups = { | |
| ... "BGroup" : ["B"], | |
| ... "@MMK_L_CGroup" : ["C"], | |
| ... "@MMK_R_CGroup" : ["C"], | |
| ... "DGroup" : ["D"], | |
| ... } | |
| >>> kerning, groups, maps = convertUFO1OrUFO2KerningToUFO3Kerning( | |
| ... testKerning, testGroups, []) | |
| >>> expected = { | |
| ... "A" : { | |
| ... "A": 1, | |
| ... "B": 2, | |
| ... "public.kern2.CGroup": 3, | |
| ... "public.kern2.DGroup": 4 | |
| ... }, | |
| ... "public.kern1.BGroup": { | |
| ... "A": 5, | |
| ... "B": 6, | |
| ... "public.kern2.CGroup": 7, | |
| ... "public.kern2.DGroup": 8 | |
| ... }, | |
| ... "public.kern1.CGroup": { | |
| ... "A": 9, | |
| ... "B": 10, | |
| ... "public.kern2.CGroup": 11, | |
| ... "public.kern2.DGroup": 12 | |
| ... } | |
| ... } | |
| >>> kerning == expected | |
| True | |
| >>> expected = { | |
| ... "BGroup": ["B"], | |
| ... "@MMK_L_CGroup": ["C"], | |
| ... "@MMK_R_CGroup": ["C"], | |
| ... "DGroup": ["D"], | |
| ... "public.kern1.BGroup": ["B"], | |
| ... "public.kern1.CGroup": ["C"], | |
| ... "public.kern2.CGroup": ["C"], | |
| ... "public.kern2.DGroup": ["D"], | |
| ... } | |
| >>> groups == expected | |
| True | |
| """ | |
| if __name__ == "__main__": | |
| import doctest | |
| doctest.testmod() | |