refactor: excel parse
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,407 @@
|
||||
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()
|
||||
@@ -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
|
||||
@@ -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 *
|
||||
@@ -0,0 +1,356 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
"""
|
||||
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: set[str] = {
|
||||
"\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: set[str] = {
|
||||
"aux",
|
||||
"clock$",
|
||||
"com1",
|
||||
"com2",
|
||||
"com3",
|
||||
"com4",
|
||||
"com5",
|
||||
"com6",
|
||||
"com7",
|
||||
"com8",
|
||||
"com9",
|
||||
"con",
|
||||
"lpt1",
|
||||
"lpt2",
|
||||
"lpt3",
|
||||
"lpt4",
|
||||
"lpt5",
|
||||
"lpt6",
|
||||
"lpt7",
|
||||
"lpt8",
|
||||
"lpt9",
|
||||
"nul",
|
||||
"prn",
|
||||
}
|
||||
maxFileNameLength: int = 255
|
||||
|
||||
|
||||
class NameTranslationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def userNameToFileName(
|
||||
userName: str, existing: Iterable[str] = (), prefix: str = "", suffix: str = ""
|
||||
) -> str:
|
||||
"""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: str, existing: Iterable[str] = [], prefix: str = "", suffix: str = ""
|
||||
) -> str:
|
||||
"""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: Iterable[str] = [], prefix: str = "", suffix: str = ""
|
||||
) -> str:
|
||||
"""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
@@ -0,0 +1,141 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from fontTools.annotations import KerningPair, KerningDict, KerningGroups, IntFloat
|
||||
|
||||
StrDict = dict[str, str]
|
||||
|
||||
|
||||
def lookupKerningValue(
|
||||
pair: KerningPair,
|
||||
kerning: KerningDict,
|
||||
groups: KerningGroups,
|
||||
fallback: IntFloat = 0,
|
||||
glyphToFirstGroup: Optional[StrDict] = None,
|
||||
glyphToSecondGroup: Optional[StrDict] = None,
|
||||
) -> IntFloat:
|
||||
"""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]
|
||||
# ensure both or no glyph-to-group mappings are provided
|
||||
if (glyphToFirstGroup is None) != (glyphToSecondGroup is None):
|
||||
raise ValueError(
|
||||
"Must provide both 'glyphToFirstGroup' and 'glyphToSecondGroup', or neither."
|
||||
)
|
||||
# create glyph to group mapping
|
||||
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
|
||||
# ensure type safety for mappings
|
||||
assert glyphToFirstGroup is not None
|
||||
assert glyphToSecondGroup is not None
|
||||
# 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
|
||||
firstGlyph = None
|
||||
else:
|
||||
firstGroup = glyphToFirstGroup.get(first)
|
||||
firstGlyph = first
|
||||
if second.startswith("public.kern2."):
|
||||
secondGroup = second
|
||||
secondGlyph = None
|
||||
else:
|
||||
secondGroup = glyphToSecondGroup.get(second)
|
||||
secondGlyph = second
|
||||
# make an ordered list of pairs to look up
|
||||
pairs = [
|
||||
(a, b)
|
||||
for a in (firstGlyph, firstGroup)
|
||||
for b in (secondGlyph, secondGroup)
|
||||
if a is not None and b is not None
|
||||
]
|
||||
# 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()
|
||||
@@ -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)
|
||||
@@ -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 *
|
||||
@@ -0,0 +1,107 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Type, TypeVar, Union, cast
|
||||
from collections.abc import Callable
|
||||
import enum
|
||||
import functools
|
||||
import warnings
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., object])
|
||||
FormatVersion = TypeVar("FormatVersion", bound="BaseFormatVersion")
|
||||
FormatVersionInput = Optional[Union[int, tuple[int, int], FormatVersion]]
|
||||
|
||||
numberTypes = (int, float)
|
||||
|
||||
|
||||
def deprecated(msg: str = "") -> Callable[[F], F]:
|
||||
"""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: F) -> F:
|
||||
@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 cast(F, wrapper)
|
||||
|
||||
return deprecated_decorator
|
||||
|
||||
|
||||
def normalizeFormatVersion(
|
||||
value: FormatVersionInput, cls: Type[FormatVersion]
|
||||
) -> FormatVersion:
|
||||
# Needed for type safety of UFOFormatVersion and GLIFFormatVersion input
|
||||
if value is None:
|
||||
return cls.default()
|
||||
if isinstance(value, cls):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
return cls((value, 0))
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
return cls(value)
|
||||
raise ValueError(f"Unsupported format version: {value!r}")
|
||||
|
||||
|
||||
# Base class for UFOFormatVersion and GLIFFormatVersion
|
||||
class BaseFormatVersion(tuple[int, int], enum.Enum):
|
||||
value: tuple[int, int]
|
||||
|
||||
def __new__(cls: Type[FormatVersion], value: tuple[int, int]) -> BaseFormatVersion:
|
||||
return super().__new__(cls, value)
|
||||
|
||||
@property
|
||||
def major(self) -> int:
|
||||
return self.value[0]
|
||||
|
||||
@property
|
||||
def minor(self) -> int:
|
||||
return self.value[1]
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value: object) -> BaseFormatVersion:
|
||||
# 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()
|
||||
raise ValueError(f"{value!r} is not a valid {cls.__name__}")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.major}.{self.minor}"
|
||||
|
||||
@classmethod
|
||||
def default(cls: Type[FormatVersion]) -> FormatVersion:
|
||||
# get the latest defined version (i.e. the max of all versions)
|
||||
return max(cls.__members__.values())
|
||||
|
||||
@classmethod
|
||||
def supported_versions(cls: Type[FormatVersion]) -> frozenset[FormatVersion]:
|
||||
return frozenset(cls.__members__.values())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user