""" 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, groups, glyphSet=()): """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 = {} 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 = {} 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): """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, groupNames, counter=0): """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()