|
""" |
|
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. |
|
""" |
|
|
|
|
|
|
|
|
|
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. |
|
|
|
""" |
|
|
|
firstReferencedGroups, secondReferencedGroups = findKnownKerningGroups(groups) |
|
|
|
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) |
|
|
|
firstRenamedGroups = {} |
|
for first in firstReferencedGroups: |
|
|
|
existingGroupNames = list(groups.keys()) + list(firstRenamedGroups.keys()) |
|
|
|
newName = first.replace("@MMK_L_", "") |
|
|
|
newName = "public.kern1." + newName |
|
|
|
newName = makeUniqueGroupName(newName, existingGroupNames) |
|
|
|
firstRenamedGroups[first] = newName |
|
secondRenamedGroups = {} |
|
for second in secondReferencedGroups: |
|
|
|
existingGroupNames = list(groups.keys()) + list(secondRenamedGroups.keys()) |
|
|
|
newName = second.replace("@MMK_R_", "") |
|
|
|
newName = "public.kern2." + newName |
|
|
|
newName = makeUniqueGroupName(newName, existingGroupNames) |
|
|
|
secondRenamedGroups[second] = newName |
|
|
|
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 |
|
|
|
|
|
allRenamedGroups = list(firstRenamedGroups.items()) |
|
allRenamedGroups += list(secondRenamedGroups.items()) |
|
for oldName, newName in allRenamedGroups: |
|
group = list(groups[oldName]) |
|
groups[newName] = group |
|
|
|
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. |
|
""" |
|
|
|
newName = name |
|
if counter > 0: |
|
newName = "%s%d" % (newName, counter) |
|
|
|
if newName in groupNames: |
|
return makeUniqueGroupName(name, groupNames, counter + 1) |
|
|
|
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() |
|
|