up follow livre

This commit is contained in:
Tykayn 2025-08-30 18:14:14 +02:00 committed by tykayn
parent b4b4398bb0
commit 3a7a3849ae
12242 changed files with 2564461 additions and 6914 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,398 @@
"""
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()

View file

@ -0,0 +1,30 @@
from __future__ import annotations
class UFOLibError(Exception):
pass
class UnsupportedUFOFormat(UFOLibError):
pass
class GlifLibError(UFOLibError):
"""An error raised by glifLib.
This class is a loose backport of PEP 678, adding a :attr:`.note`
attribute that can hold additional context for errors encountered.
It will be maintained until only Python 3.11-and-later are supported.
"""
def _add_note(self, note: str) -> None:
# Loose backport of PEP 678 until we only support Python 3.11+, used for
# adding additional context to errors.
# TODO: Replace with https://docs.python.org/3.11/library/exceptions.html#BaseException.add_note
(message, *rest) = self.args
self.args = ((message + "\n" + note), *rest)
class UnsupportedGLIFFormat(GlifLibError):
pass

View file

@ -0,0 +1,6 @@
"""DEPRECATED - This module is kept here only as a backward compatibility shim
for the old ufoLib.etree module, which was moved to :mod:`fontTools.misc.etree`.
Please use the latter instead.
"""
from fontTools.misc.etree import *

View file

@ -0,0 +1,346 @@
"""
Convert user-provided internal UFO names to spec-compliant filenames.
This module implements the algorithm for converting between a "user name" -
something that a user can choose arbitrarily inside a font editor - and a file
name suitable for use in a wide range of operating systems and filesystems.
The `UFO 3 specification <http://unifiedfontobject.org/versions/ufo3/conventions/>`_
provides an example of an algorithm for such conversion, which avoids illegal
characters, reserved file names, ambiguity between upper- and lower-case
characters, and clashes with existing files.
This code was originally copied from
`ufoLib <https://github.com/unified-font-object/ufoLib/blob/8747da7/Lib/ufoLib/filenames.py>`_
by Tal Leming and is copyright (c) 2005-2016, The RoboFab Developers:
- Erik van Blokland
- Tal Leming
- Just van Rossum
"""
# Restrictions are taken mostly from
# https://docs.microsoft.com/en-gb/windows/win32/fileio/naming-a-file#naming-conventions.
#
# 1. Integer value zero, sometimes referred to as the ASCII NUL character.
# 2. Characters whose integer representations are in the range 1 to 31,
# inclusive.
# 3. Various characters that (mostly) Windows and POSIX-y filesystems don't
# allow, plus "(" and ")", as per the specification.
illegalCharacters = {
"\x00",
"\x01",
"\x02",
"\x03",
"\x04",
"\x05",
"\x06",
"\x07",
"\x08",
"\t",
"\n",
"\x0b",
"\x0c",
"\r",
"\x0e",
"\x0f",
"\x10",
"\x11",
"\x12",
"\x13",
"\x14",
"\x15",
"\x16",
"\x17",
"\x18",
"\x19",
"\x1a",
"\x1b",
"\x1c",
"\x1d",
"\x1e",
"\x1f",
'"',
"*",
"+",
"/",
":",
"<",
">",
"?",
"[",
"\\",
"]",
"(",
")",
"|",
"\x7f",
}
reservedFileNames = {
"aux",
"clock$",
"com1",
"com2",
"com3",
"com4",
"com5",
"com6",
"com7",
"com8",
"com9",
"con",
"lpt1",
"lpt2",
"lpt3",
"lpt4",
"lpt5",
"lpt6",
"lpt7",
"lpt8",
"lpt9",
"nul",
"prn",
}
maxFileNameLength = 255
class NameTranslationError(Exception):
pass
def userNameToFileName(userName: str, existing=(), prefix="", suffix=""):
"""Converts from a user name to a file name.
Takes care to avoid illegal characters, reserved file names, ambiguity between
upper- and lower-case characters, and clashes with existing files.
Args:
userName (str): The input file name.
existing: A case-insensitive list of all existing file names.
prefix: Prefix to be prepended to the file name.
suffix: Suffix to be appended to the file name.
Returns:
A suitable filename.
Raises:
NameTranslationError: If no suitable name could be generated.
Examples::
>>> userNameToFileName("a") == "a"
True
>>> userNameToFileName("A") == "A_"
True
>>> userNameToFileName("AE") == "A_E_"
True
>>> userNameToFileName("Ae") == "A_e"
True
>>> userNameToFileName("ae") == "ae"
True
>>> userNameToFileName("aE") == "aE_"
True
>>> userNameToFileName("a.alt") == "a.alt"
True
>>> userNameToFileName("A.alt") == "A_.alt"
True
>>> userNameToFileName("A.Alt") == "A_.A_lt"
True
>>> userNameToFileName("A.aLt") == "A_.aL_t"
True
>>> userNameToFileName(u"A.alT") == "A_.alT_"
True
>>> userNameToFileName("T_H") == "T__H_"
True
>>> userNameToFileName("T_h") == "T__h"
True
>>> userNameToFileName("t_h") == "t_h"
True
>>> userNameToFileName("F_F_I") == "F__F__I_"
True
>>> userNameToFileName("f_f_i") == "f_f_i"
True
>>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash"
True
>>> userNameToFileName(".notdef") == "_notdef"
True
>>> userNameToFileName("con") == "_con"
True
>>> userNameToFileName("CON") == "C_O_N_"
True
>>> userNameToFileName("con.alt") == "_con.alt"
True
>>> userNameToFileName("alt.con") == "alt._con"
True
"""
# the incoming name must be a string
if not isinstance(userName, str):
raise ValueError("The value for userName must be a string.")
# establish the prefix and suffix lengths
prefixLength = len(prefix)
suffixLength = len(suffix)
# replace an initial period with an _
# if no prefix is to be added
if not prefix and userName[0] == ".":
userName = "_" + userName[1:]
# filter the user name
filteredUserName = []
for character in userName:
# replace illegal characters with _
if character in illegalCharacters:
character = "_"
# add _ to all non-lower characters
elif character != character.lower():
character += "_"
filteredUserName.append(character)
userName = "".join(filteredUserName)
# clip to 255
sliceLength = maxFileNameLength - prefixLength - suffixLength
userName = userName[:sliceLength]
# test for illegal files names
parts = []
for part in userName.split("."):
if part.lower() in reservedFileNames:
part = "_" + part
parts.append(part)
userName = ".".join(parts)
# test for clash
fullName = prefix + userName + suffix
if fullName.lower() in existing:
fullName = handleClash1(userName, existing, prefix, suffix)
# finished
return fullName
def handleClash1(userName, existing=[], prefix="", suffix=""):
"""A helper function that resolves collisions with existing names when choosing a filename.
This function attempts to append an unused integer counter to the filename.
Args:
userName (str): The input file name.
existing: A case-insensitive list of all existing file names.
prefix: Prefix to be prepended to the file name.
suffix: Suffix to be appended to the file name.
Returns:
A suitable filename.
>>> prefix = ("0" * 5) + "."
>>> suffix = "." + ("0" * 10)
>>> existing = ["a" * 5]
>>> e = list(existing)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000001.0000000000')
True
>>> e = list(existing)
>>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000002.0000000000')
True
>>> e = list(existing)
>>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix)
>>> handleClash1(userName="A" * 5, existing=e,
... prefix=prefix, suffix=suffix) == (
... '00000.AAAAA000000000000001.0000000000')
True
"""
# if the prefix length + user name length + suffix length + 15 is at
# or past the maximum length, silce 15 characters off of the user name
prefixLength = len(prefix)
suffixLength = len(suffix)
if prefixLength + len(userName) + suffixLength + 15 > maxFileNameLength:
l = prefixLength + len(userName) + suffixLength + 15
sliceLength = maxFileNameLength - l
userName = userName[:sliceLength]
finalName = None
# try to add numbers to create a unique name
counter = 1
while finalName is None:
name = userName + str(counter).zfill(15)
fullName = prefix + name + suffix
if fullName.lower() not in existing:
finalName = fullName
break
else:
counter += 1
if counter >= 999999999999999:
break
# if there is a clash, go to the next fallback
if finalName is None:
finalName = handleClash2(existing, prefix, suffix)
# finished
return finalName
def handleClash2(existing=[], prefix="", suffix=""):
"""A helper function that resolves collisions with existing names when choosing a filename.
This function is a fallback to :func:`handleClash1`. It attempts to append an unused integer counter to the filename.
Args:
userName (str): The input file name.
existing: A case-insensitive list of all existing file names.
prefix: Prefix to be prepended to the file name.
suffix: Suffix to be appended to the file name.
Returns:
A suitable filename.
Raises:
NameTranslationError: If no suitable name could be generated.
Examples::
>>> prefix = ("0" * 5) + "."
>>> suffix = "." + ("0" * 10)
>>> existing = [prefix + str(i) + suffix for i in range(100)]
>>> e = list(existing)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.100.0000000000')
True
>>> e = list(existing)
>>> e.remove(prefix + "1" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.1.0000000000')
True
>>> e = list(existing)
>>> e.remove(prefix + "2" + suffix)
>>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == (
... '00000.2.0000000000')
True
"""
# calculate the longest possible string
maxLength = maxFileNameLength - len(prefix) - len(suffix)
maxValue = int("9" * maxLength)
# try to find a number
finalName = None
counter = 1
while finalName is None:
fullName = prefix + str(counter) + suffix
if fullName.lower() not in existing:
finalName = fullName
break
else:
counter += 1
if counter >= maxValue:
break
# raise an error if nothing has been found
if finalName is None:
raise NameTranslationError("No unique name could be found.")
# finished
return finalName
if __name__ == "__main__":
import doctest
doctest.testmod()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,121 @@
def lookupKerningValue(
pair, kerning, groups, fallback=0, glyphToFirstGroup=None, glyphToSecondGroup=None
):
"""Retrieve the kerning value (if any) between a pair of elements.
The elments can be either individual glyphs (by name) or kerning
groups (by name), or any combination of the two.
Args:
pair:
A tuple, in logical order (first, second) with respect
to the reading direction, to query the font for kerning
information on. Each element in the tuple can be either
a glyph name or a kerning group name.
kerning:
A dictionary of kerning pairs.
groups:
A set of kerning groups.
fallback:
The fallback value to return if no kern is found between
the elements in ``pair``. Defaults to 0.
glyphToFirstGroup:
A dictionary mapping glyph names to the first-glyph kerning
groups to which they belong. Defaults to ``None``.
glyphToSecondGroup:
A dictionary mapping glyph names to the second-glyph kerning
groups to which they belong. Defaults to ``None``.
Returns:
The kerning value between the element pair. If no kerning for
the pair is found, the fallback value is returned.
Note: This function expects the ``kerning`` argument to be a flat
dictionary of kerning pairs, not the nested structure used in a
kerning.plist file.
Examples::
>>> groups = {
... "public.kern1.O" : ["O", "D", "Q"],
... "public.kern2.E" : ["E", "F"]
... }
>>> kerning = {
... ("public.kern1.O", "public.kern2.E") : -100,
... ("public.kern1.O", "F") : -200,
... ("D", "F") : -300
... }
>>> lookupKerningValue(("D", "F"), kerning, groups)
-300
>>> lookupKerningValue(("O", "F"), kerning, groups)
-200
>>> lookupKerningValue(("O", "E"), kerning, groups)
-100
>>> lookupKerningValue(("O", "O"), kerning, groups)
0
>>> lookupKerningValue(("E", "E"), kerning, groups)
0
>>> lookupKerningValue(("E", "O"), kerning, groups)
0
>>> lookupKerningValue(("X", "X"), kerning, groups)
0
>>> lookupKerningValue(("public.kern1.O", "public.kern2.E"),
... kerning, groups)
-100
>>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups)
-200
>>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups)
-100
>>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups)
0
"""
# quickly check to see if the pair is in the kerning dictionary
if pair in kerning:
return kerning[pair]
# create glyph to group mapping
if glyphToFirstGroup is not None:
assert glyphToSecondGroup is not None
if glyphToSecondGroup is not None:
assert glyphToFirstGroup is not None
if glyphToFirstGroup is None:
glyphToFirstGroup = {}
glyphToSecondGroup = {}
for group, groupMembers in groups.items():
if group.startswith("public.kern1."):
for glyph in groupMembers:
glyphToFirstGroup[glyph] = group
elif group.startswith("public.kern2."):
for glyph in groupMembers:
glyphToSecondGroup[glyph] = group
# get group names and make sure first and second are glyph names
first, second = pair
firstGroup = secondGroup = None
if first.startswith("public.kern1."):
firstGroup = first
first = None
else:
firstGroup = glyphToFirstGroup.get(first)
if second.startswith("public.kern2."):
secondGroup = second
second = None
else:
secondGroup = glyphToSecondGroup.get(second)
# make an ordered list of pairs to look up
pairs = [
(first, second),
(first, secondGroup),
(firstGroup, second),
(firstGroup, secondGroup),
]
# look up the pairs and return any matches
for pair in pairs:
if pair in kerning:
return kerning[pair]
# use the fallback value
return fallback
if __name__ == "__main__":
import doctest
doctest.testmod()

View file

@ -0,0 +1,47 @@
"""DEPRECATED - This module is kept here only as a backward compatibility shim
for the old `ufoLib.plistlib` module, which was moved to :class:`fontTools.misc.plistlib`.
Please use the latter instead.
"""
from fontTools.misc.plistlib import dump, dumps, load, loads
from fontTools.misc.textTools import tobytes
# The following functions were part of the old py2-like ufoLib.plistlib API.
# They are kept only for backward compatiblity.
from fontTools.ufoLib.utils import deprecated
@deprecated("Use 'fontTools.misc.plistlib.load' instead")
def readPlist(path_or_file):
did_open = False
if isinstance(path_or_file, str):
path_or_file = open(path_or_file, "rb")
did_open = True
try:
return load(path_or_file, use_builtin_types=False)
finally:
if did_open:
path_or_file.close()
@deprecated("Use 'fontTools.misc.plistlib.dump' instead")
def writePlist(value, path_or_file):
did_open = False
if isinstance(path_or_file, str):
path_or_file = open(path_or_file, "wb")
did_open = True
try:
dump(value, path_or_file, use_builtin_types=False)
finally:
if did_open:
path_or_file.close()
@deprecated("Use 'fontTools.misc.plistlib.loads' instead")
def readPlistFromString(data):
return loads(tobytes(data, encoding="utf-8"), use_builtin_types=False)
@deprecated("Use 'fontTools.misc.plistlib.dumps' instead")
def writePlistToString(value):
return dumps(value, use_builtin_types=False)

View file

@ -0,0 +1,6 @@
"""DEPRECATED - This module is kept here only as a backward compatibility shim
for the old `ufoLib.pointPen` module, which was moved to :class:`fontTools.pens.pointPen`.
Please use the latter instead.
"""
from fontTools.pens.pointPen import *

View file

@ -0,0 +1,79 @@
"""This module contains miscellaneous helpers.
It is not considered part of the public ufoLib API. It does, however,
define the :py:obj:`.deprecated` decorator that is used elsewhere in
the module.
"""
import warnings
import functools
numberTypes = (int, float)
def deprecated(msg=""):
"""Decorator factory to mark functions as deprecated with given message.
>>> @deprecated("Enough!")
... def some_function():
... "I just print 'hello world'."
... print("hello world")
>>> some_function()
hello world
>>> some_function.__doc__ == "I just print 'hello world'."
True
"""
def deprecated_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(
f"{func.__name__} function is a deprecated. {msg}",
category=DeprecationWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return wrapper
return deprecated_decorator
# To be mixed with enum.Enum in UFOFormatVersion and GLIFFormatVersion
class _VersionTupleEnumMixin:
@property
def major(self):
return self.value[0]
@property
def minor(self):
return self.value[1]
@classmethod
def _missing_(cls, value):
# allow to initialize a version enum from a single (major) integer
if isinstance(value, int):
return cls((value, 0))
# or from None to obtain the current default version
if value is None:
return cls.default()
return super()._missing_(value)
def __str__(self):
return f"{self.major}.{self.minor}"
@classmethod
def default(cls):
# get the latest defined version (i.e. the max of all versions)
return max(cls.__members__.values())
@classmethod
def supported_versions(cls):
return frozenset(cls.__members__.values())
if __name__ == "__main__":
import doctest
doctest.testmod()

File diff suppressed because it is too large Load diff