Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/fontTools/__init__.py +8 -0
- lib/python3.12/site-packages/fontTools/__main__.py +35 -0
- lib/python3.12/site-packages/fontTools/afmLib.py +439 -0
- lib/python3.12/site-packages/fontTools/agl.py +0 -0
- lib/python3.12/site-packages/fontTools/annotations.py +30 -0
- lib/python3.12/site-packages/fontTools/colorLib/__init__.py +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/colorLib/builder.py +664 -0
- lib/python3.12/site-packages/fontTools/colorLib/errors.py +2 -0
- lib/python3.12/site-packages/fontTools/colorLib/geometry.py +143 -0
- lib/python3.12/site-packages/fontTools/colorLib/table_builder.py +223 -0
- lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py +81 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py +0 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/__main__.py +6 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/__main__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/split.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/statNames.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/types.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/split.py +475 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/statNames.py +260 -0
- lib/python3.12/site-packages/fontTools/designspaceLib/types.py +147 -0
- lib/python3.12/site-packages/fontTools/encodings/MacRoman.py +258 -0
- lib/python3.12/site-packages/fontTools/encodings/StandardEncoding.py +258 -0
- lib/python3.12/site-packages/fontTools/encodings/__init__.py +1 -0
- lib/python3.12/site-packages/fontTools/encodings/__pycache__/MacRoman.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/encodings/__pycache__/StandardEncoding.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/encodings/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/encodings/__pycache__/codecs.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/encodings/codecs.py +135 -0
- lib/python3.12/site-packages/fontTools/fontBuilder.py +1014 -0
- lib/python3.12/site-packages/fontTools/help.py +36 -0
- lib/python3.12/site-packages/fontTools/mtiLib/__init__.py +1400 -0
- lib/python3.12/site-packages/fontTools/mtiLib/__main__.py +5 -0
- lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__main__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/otlLib/__init__.py +1 -0
- lib/python3.12/site-packages/fontTools/otlLib/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/otlLib/__pycache__/error.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/otlLib/__pycache__/maxContextCalc.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/fontTools/otlLib/builder.py +0 -0
- lib/python3.12/site-packages/fontTools/otlLib/error.py +11 -0
- lib/python3.12/site-packages/fontTools/otlLib/maxContextCalc.py +96 -0
- lib/python3.12/site-packages/fontTools/otlLib/optimize/__init__.py +53 -0
- lib/python3.12/site-packages/fontTools/otlLib/optimize/__main__.py +6 -0
- lib/python3.12/site-packages/fontTools/otlLib/optimize/__pycache__/__init__.cpython-312.pyc +0 -0
lib/python3.12/site-packages/fontTools/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from fontTools.misc.loggingTools import configLogger
|
| 3 |
+
|
| 4 |
+
log = logging.getLogger(__name__)
|
| 5 |
+
|
| 6 |
+
version = __version__ = "4.61.1"
|
| 7 |
+
|
| 8 |
+
__all__ = ["version", "log", "configLogger"]
|
lib/python3.12/site-packages/fontTools/__main__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def main(args=None):
|
| 5 |
+
if args is None:
|
| 6 |
+
args = sys.argv[1:]
|
| 7 |
+
|
| 8 |
+
# TODO Handle library-wide options. Eg.:
|
| 9 |
+
# --unicodedata
|
| 10 |
+
# --verbose / other logging stuff
|
| 11 |
+
|
| 12 |
+
# TODO Allow a way to run arbitrary modules? Useful for setting
|
| 13 |
+
# library-wide options and calling another library. Eg.:
|
| 14 |
+
#
|
| 15 |
+
# $ fonttools --unicodedata=... fontmake ...
|
| 16 |
+
#
|
| 17 |
+
# This allows for a git-like command where thirdparty commands
|
| 18 |
+
# can be added. Should we just try importing the fonttools
|
| 19 |
+
# module first and try without if it fails?
|
| 20 |
+
|
| 21 |
+
if len(sys.argv) < 2:
|
| 22 |
+
sys.argv.append("help")
|
| 23 |
+
if sys.argv[1] == "-h" or sys.argv[1] == "--help":
|
| 24 |
+
sys.argv[1] = "help"
|
| 25 |
+
mod = "fontTools." + sys.argv[1]
|
| 26 |
+
sys.argv[1] = sys.argv[0] + " " + sys.argv[1]
|
| 27 |
+
del sys.argv[0]
|
| 28 |
+
|
| 29 |
+
import runpy
|
| 30 |
+
|
| 31 |
+
runpy.run_module(mod, run_name="__main__")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
sys.exit(main())
|
lib/python3.12/site-packages/fontTools/afmLib.py
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module for reading and writing AFM (Adobe Font Metrics) files.
|
| 2 |
+
|
| 3 |
+
Note that this has been designed to read in AFM files generated by Fontographer
|
| 4 |
+
and has not been tested on many other files. In particular, it does not
|
| 5 |
+
implement the whole Adobe AFM specification [#f1]_ but, it should read most
|
| 6 |
+
"common" AFM files.
|
| 7 |
+
|
| 8 |
+
Here is an example of using `afmLib` to read, modify and write an AFM file:
|
| 9 |
+
|
| 10 |
+
>>> from fontTools.afmLib import AFM
|
| 11 |
+
>>> f = AFM("Tests/afmLib/data/TestAFM.afm")
|
| 12 |
+
>>>
|
| 13 |
+
>>> # Accessing a pair gets you the kern value
|
| 14 |
+
>>> f[("V","A")]
|
| 15 |
+
-60
|
| 16 |
+
>>>
|
| 17 |
+
>>> # Accessing a glyph name gets you metrics
|
| 18 |
+
>>> f["A"]
|
| 19 |
+
(65, 668, (8, -25, 660, 666))
|
| 20 |
+
>>> # (charnum, width, bounding box)
|
| 21 |
+
>>>
|
| 22 |
+
>>> # Accessing an attribute gets you metadata
|
| 23 |
+
>>> f.FontName
|
| 24 |
+
'TestFont-Regular'
|
| 25 |
+
>>> f.FamilyName
|
| 26 |
+
'TestFont'
|
| 27 |
+
>>> f.Weight
|
| 28 |
+
'Regular'
|
| 29 |
+
>>> f.XHeight
|
| 30 |
+
500
|
| 31 |
+
>>> f.Ascender
|
| 32 |
+
750
|
| 33 |
+
>>>
|
| 34 |
+
>>> # Attributes and items can also be set
|
| 35 |
+
>>> f[("A","V")] = -150 # Tighten kerning
|
| 36 |
+
>>> f.FontName = "TestFont Squished"
|
| 37 |
+
>>>
|
| 38 |
+
>>> # And the font written out again (remove the # in front)
|
| 39 |
+
>>> #f.write("testfont-squished.afm")
|
| 40 |
+
|
| 41 |
+
.. rubric:: Footnotes
|
| 42 |
+
|
| 43 |
+
.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
|
| 44 |
+
Adobe Font Metrics File Format Specification.
|
| 45 |
+
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
import re
|
| 49 |
+
|
| 50 |
+
# every single line starts with a "word"
|
| 51 |
+
identifierRE = re.compile(r"^([A-Za-z]+).*")
|
| 52 |
+
|
| 53 |
+
# regular expression to parse char lines
|
| 54 |
+
charRE = re.compile(
|
| 55 |
+
r"(-?\d+)" # charnum
|
| 56 |
+
r"\s*;\s*WX\s+" # ; WX
|
| 57 |
+
r"(-?\d+)" # width
|
| 58 |
+
r"\s*;\s*N\s+" # ; N
|
| 59 |
+
r"([.A-Za-z0-9_]+)" # charname
|
| 60 |
+
r"\s*;\s*B\s+" # ; B
|
| 61 |
+
r"(-?\d+)" # left
|
| 62 |
+
r"\s+"
|
| 63 |
+
r"(-?\d+)" # bottom
|
| 64 |
+
r"\s+"
|
| 65 |
+
r"(-?\d+)" # right
|
| 66 |
+
r"\s+"
|
| 67 |
+
r"(-?\d+)" # top
|
| 68 |
+
r"\s*;\s*" # ;
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
# regular expression to parse kerning lines
|
| 72 |
+
kernRE = re.compile(
|
| 73 |
+
r"([.A-Za-z0-9_]+)" # leftchar
|
| 74 |
+
r"\s+"
|
| 75 |
+
r"([.A-Za-z0-9_]+)" # rightchar
|
| 76 |
+
r"\s+"
|
| 77 |
+
r"(-?\d+)" # value
|
| 78 |
+
r"\s*"
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# regular expressions to parse composite info lines of the form:
|
| 82 |
+
# Aacute 2 ; PCC A 0 0 ; PCC acute 182 211 ;
|
| 83 |
+
compositeRE = re.compile(
|
| 84 |
+
r"([.A-Za-z0-9_]+)" # char name
|
| 85 |
+
r"\s+"
|
| 86 |
+
r"(\d+)" # number of parts
|
| 87 |
+
r"\s*;\s*"
|
| 88 |
+
)
|
| 89 |
+
componentRE = re.compile(
|
| 90 |
+
r"PCC\s+" # PPC
|
| 91 |
+
r"([.A-Za-z0-9_]+)" # base char name
|
| 92 |
+
r"\s+"
|
| 93 |
+
r"(-?\d+)" # x offset
|
| 94 |
+
r"\s+"
|
| 95 |
+
r"(-?\d+)" # y offset
|
| 96 |
+
r"\s*;\s*"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
preferredAttributeOrder = [
|
| 100 |
+
"FontName",
|
| 101 |
+
"FullName",
|
| 102 |
+
"FamilyName",
|
| 103 |
+
"Weight",
|
| 104 |
+
"ItalicAngle",
|
| 105 |
+
"IsFixedPitch",
|
| 106 |
+
"FontBBox",
|
| 107 |
+
"UnderlinePosition",
|
| 108 |
+
"UnderlineThickness",
|
| 109 |
+
"Version",
|
| 110 |
+
"Notice",
|
| 111 |
+
"EncodingScheme",
|
| 112 |
+
"CapHeight",
|
| 113 |
+
"XHeight",
|
| 114 |
+
"Ascender",
|
| 115 |
+
"Descender",
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class error(Exception):
|
| 120 |
+
pass
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class AFM(object):
|
| 124 |
+
_attrs = None
|
| 125 |
+
|
| 126 |
+
_keywords = [
|
| 127 |
+
"StartFontMetrics",
|
| 128 |
+
"EndFontMetrics",
|
| 129 |
+
"StartCharMetrics",
|
| 130 |
+
"EndCharMetrics",
|
| 131 |
+
"StartKernData",
|
| 132 |
+
"StartKernPairs",
|
| 133 |
+
"EndKernPairs",
|
| 134 |
+
"EndKernData",
|
| 135 |
+
"StartComposites",
|
| 136 |
+
"EndComposites",
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
def __init__(self, path=None):
|
| 140 |
+
"""AFM file reader.
|
| 141 |
+
|
| 142 |
+
Instantiating an object with a path name will cause the file to be opened,
|
| 143 |
+
read, and parsed. Alternatively the path can be left unspecified, and a
|
| 144 |
+
file can be parsed later with the :meth:`read` method."""
|
| 145 |
+
self._attrs = {}
|
| 146 |
+
self._chars = {}
|
| 147 |
+
self._kerning = {}
|
| 148 |
+
self._index = {}
|
| 149 |
+
self._comments = []
|
| 150 |
+
self._composites = {}
|
| 151 |
+
if path is not None:
|
| 152 |
+
self.read(path)
|
| 153 |
+
|
| 154 |
+
def read(self, path):
|
| 155 |
+
"""Opens, reads and parses a file."""
|
| 156 |
+
lines = readlines(path)
|
| 157 |
+
for line in lines:
|
| 158 |
+
if not line.strip():
|
| 159 |
+
continue
|
| 160 |
+
m = identifierRE.match(line)
|
| 161 |
+
if m is None:
|
| 162 |
+
raise error("syntax error in AFM file: " + repr(line))
|
| 163 |
+
|
| 164 |
+
pos = m.regs[1][1]
|
| 165 |
+
word = line[:pos]
|
| 166 |
+
rest = line[pos:].strip()
|
| 167 |
+
if word in self._keywords:
|
| 168 |
+
continue
|
| 169 |
+
if word == "C":
|
| 170 |
+
self.parsechar(rest)
|
| 171 |
+
elif word == "KPX":
|
| 172 |
+
self.parsekernpair(rest)
|
| 173 |
+
elif word == "CC":
|
| 174 |
+
self.parsecomposite(rest)
|
| 175 |
+
else:
|
| 176 |
+
self.parseattr(word, rest)
|
| 177 |
+
|
| 178 |
+
def parsechar(self, rest):
|
| 179 |
+
m = charRE.match(rest)
|
| 180 |
+
if m is None:
|
| 181 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 182 |
+
things = []
|
| 183 |
+
for fr, to in m.regs[1:]:
|
| 184 |
+
things.append(rest[fr:to])
|
| 185 |
+
charname = things[2]
|
| 186 |
+
del things[2]
|
| 187 |
+
charnum, width, l, b, r, t = (int(thing) for thing in things)
|
| 188 |
+
self._chars[charname] = charnum, width, (l, b, r, t)
|
| 189 |
+
|
| 190 |
+
def parsekernpair(self, rest):
|
| 191 |
+
m = kernRE.match(rest)
|
| 192 |
+
if m is None:
|
| 193 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 194 |
+
things = []
|
| 195 |
+
for fr, to in m.regs[1:]:
|
| 196 |
+
things.append(rest[fr:to])
|
| 197 |
+
leftchar, rightchar, value = things
|
| 198 |
+
value = int(value)
|
| 199 |
+
self._kerning[(leftchar, rightchar)] = value
|
| 200 |
+
|
| 201 |
+
def parseattr(self, word, rest):
|
| 202 |
+
if word == "FontBBox":
|
| 203 |
+
l, b, r, t = [int(thing) for thing in rest.split()]
|
| 204 |
+
self._attrs[word] = l, b, r, t
|
| 205 |
+
elif word == "Comment":
|
| 206 |
+
self._comments.append(rest)
|
| 207 |
+
else:
|
| 208 |
+
try:
|
| 209 |
+
value = int(rest)
|
| 210 |
+
except (ValueError, OverflowError):
|
| 211 |
+
self._attrs[word] = rest
|
| 212 |
+
else:
|
| 213 |
+
self._attrs[word] = value
|
| 214 |
+
|
| 215 |
+
def parsecomposite(self, rest):
|
| 216 |
+
m = compositeRE.match(rest)
|
| 217 |
+
if m is None:
|
| 218 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 219 |
+
charname = m.group(1)
|
| 220 |
+
ncomponents = int(m.group(2))
|
| 221 |
+
rest = rest[m.regs[0][1] :]
|
| 222 |
+
components = []
|
| 223 |
+
while True:
|
| 224 |
+
m = componentRE.match(rest)
|
| 225 |
+
if m is None:
|
| 226 |
+
raise error("syntax error in AFM file: " + repr(rest))
|
| 227 |
+
basechar = m.group(1)
|
| 228 |
+
xoffset = int(m.group(2))
|
| 229 |
+
yoffset = int(m.group(3))
|
| 230 |
+
components.append((basechar, xoffset, yoffset))
|
| 231 |
+
rest = rest[m.regs[0][1] :]
|
| 232 |
+
if not rest:
|
| 233 |
+
break
|
| 234 |
+
assert len(components) == ncomponents
|
| 235 |
+
self._composites[charname] = components
|
| 236 |
+
|
| 237 |
+
def write(self, path, sep="\r"):
|
| 238 |
+
"""Writes out an AFM font to the given path."""
|
| 239 |
+
import time
|
| 240 |
+
|
| 241 |
+
lines = [
|
| 242 |
+
"StartFontMetrics 2.0",
|
| 243 |
+
"Comment Generated by afmLib; at %s"
|
| 244 |
+
% (time.strftime("%m/%d/%Y %H:%M:%S", time.localtime(time.time()))),
|
| 245 |
+
]
|
| 246 |
+
|
| 247 |
+
# write comments, assuming (possibly wrongly!) they should
|
| 248 |
+
# all appear at the top
|
| 249 |
+
for comment in self._comments:
|
| 250 |
+
lines.append("Comment " + comment)
|
| 251 |
+
|
| 252 |
+
# write attributes, first the ones we know about, in
|
| 253 |
+
# a preferred order
|
| 254 |
+
attrs = self._attrs
|
| 255 |
+
for attr in preferredAttributeOrder:
|
| 256 |
+
if attr in attrs:
|
| 257 |
+
value = attrs[attr]
|
| 258 |
+
if attr == "FontBBox":
|
| 259 |
+
value = "%s %s %s %s" % value
|
| 260 |
+
lines.append(attr + " " + str(value))
|
| 261 |
+
# then write the attributes we don't know about,
|
| 262 |
+
# in alphabetical order
|
| 263 |
+
items = sorted(attrs.items())
|
| 264 |
+
for attr, value in items:
|
| 265 |
+
if attr in preferredAttributeOrder:
|
| 266 |
+
continue
|
| 267 |
+
lines.append(attr + " " + str(value))
|
| 268 |
+
|
| 269 |
+
# write char metrics
|
| 270 |
+
lines.append("StartCharMetrics " + repr(len(self._chars)))
|
| 271 |
+
items = [
|
| 272 |
+
(charnum, (charname, width, box))
|
| 273 |
+
for charname, (charnum, width, box) in self._chars.items()
|
| 274 |
+
]
|
| 275 |
+
|
| 276 |
+
def myKey(a):
|
| 277 |
+
"""Custom key function to make sure unencoded chars (-1)
|
| 278 |
+
end up at the end of the list after sorting."""
|
| 279 |
+
if a[0] == -1:
|
| 280 |
+
a = (0xFFFF,) + a[1:] # 0xffff is an arbitrary large number
|
| 281 |
+
return a
|
| 282 |
+
|
| 283 |
+
items.sort(key=myKey)
|
| 284 |
+
|
| 285 |
+
for charnum, (charname, width, (l, b, r, t)) in items:
|
| 286 |
+
lines.append(
|
| 287 |
+
"C %d ; WX %d ; N %s ; B %d %d %d %d ;"
|
| 288 |
+
% (charnum, width, charname, l, b, r, t)
|
| 289 |
+
)
|
| 290 |
+
lines.append("EndCharMetrics")
|
| 291 |
+
|
| 292 |
+
# write kerning info
|
| 293 |
+
lines.append("StartKernData")
|
| 294 |
+
lines.append("StartKernPairs " + repr(len(self._kerning)))
|
| 295 |
+
items = sorted(self._kerning.items())
|
| 296 |
+
for (leftchar, rightchar), value in items:
|
| 297 |
+
lines.append("KPX %s %s %d" % (leftchar, rightchar, value))
|
| 298 |
+
lines.append("EndKernPairs")
|
| 299 |
+
lines.append("EndKernData")
|
| 300 |
+
|
| 301 |
+
if self._composites:
|
| 302 |
+
composites = sorted(self._composites.items())
|
| 303 |
+
lines.append("StartComposites %s" % len(self._composites))
|
| 304 |
+
for charname, components in composites:
|
| 305 |
+
line = "CC %s %s ;" % (charname, len(components))
|
| 306 |
+
for basechar, xoffset, yoffset in components:
|
| 307 |
+
line = line + " PCC %s %s %s ;" % (basechar, xoffset, yoffset)
|
| 308 |
+
lines.append(line)
|
| 309 |
+
lines.append("EndComposites")
|
| 310 |
+
|
| 311 |
+
lines.append("EndFontMetrics")
|
| 312 |
+
|
| 313 |
+
writelines(path, lines, sep)
|
| 314 |
+
|
| 315 |
+
def has_kernpair(self, pair):
|
| 316 |
+
"""Returns `True` if the given glyph pair (specified as a tuple) exists
|
| 317 |
+
in the kerning dictionary."""
|
| 318 |
+
return pair in self._kerning
|
| 319 |
+
|
| 320 |
+
def kernpairs(self):
|
| 321 |
+
"""Returns a list of all kern pairs in the kerning dictionary."""
|
| 322 |
+
return list(self._kerning.keys())
|
| 323 |
+
|
| 324 |
+
def has_char(self, char):
|
| 325 |
+
"""Returns `True` if the given glyph exists in the font."""
|
| 326 |
+
return char in self._chars
|
| 327 |
+
|
| 328 |
+
def chars(self):
|
| 329 |
+
"""Returns a list of all glyph names in the font."""
|
| 330 |
+
return list(self._chars.keys())
|
| 331 |
+
|
| 332 |
+
def comments(self):
|
| 333 |
+
"""Returns all comments from the file."""
|
| 334 |
+
return self._comments
|
| 335 |
+
|
| 336 |
+
def addComment(self, comment):
|
| 337 |
+
"""Adds a new comment to the file."""
|
| 338 |
+
self._comments.append(comment)
|
| 339 |
+
|
| 340 |
+
def addComposite(self, glyphName, components):
|
| 341 |
+
"""Specifies that the glyph `glyphName` is made up of the given components.
|
| 342 |
+
The components list should be of the following form::
|
| 343 |
+
|
| 344 |
+
[
|
| 345 |
+
(glyphname, xOffset, yOffset),
|
| 346 |
+
...
|
| 347 |
+
]
|
| 348 |
+
|
| 349 |
+
"""
|
| 350 |
+
self._composites[glyphName] = components
|
| 351 |
+
|
| 352 |
+
def __getattr__(self, attr):
|
| 353 |
+
if attr in self._attrs:
|
| 354 |
+
return self._attrs[attr]
|
| 355 |
+
else:
|
| 356 |
+
raise AttributeError(attr)
|
| 357 |
+
|
| 358 |
+
def __setattr__(self, attr, value):
|
| 359 |
+
# all attrs *not* starting with "_" are consider to be AFM keywords
|
| 360 |
+
if attr[:1] == "_":
|
| 361 |
+
self.__dict__[attr] = value
|
| 362 |
+
else:
|
| 363 |
+
self._attrs[attr] = value
|
| 364 |
+
|
| 365 |
+
def __delattr__(self, attr):
|
| 366 |
+
# all attrs *not* starting with "_" are consider to be AFM keywords
|
| 367 |
+
if attr[:1] == "_":
|
| 368 |
+
try:
|
| 369 |
+
del self.__dict__[attr]
|
| 370 |
+
except KeyError:
|
| 371 |
+
raise AttributeError(attr)
|
| 372 |
+
else:
|
| 373 |
+
try:
|
| 374 |
+
del self._attrs[attr]
|
| 375 |
+
except KeyError:
|
| 376 |
+
raise AttributeError(attr)
|
| 377 |
+
|
| 378 |
+
def __getitem__(self, key):
|
| 379 |
+
if isinstance(key, tuple):
|
| 380 |
+
# key is a tuple, return the kernpair
|
| 381 |
+
return self._kerning[key]
|
| 382 |
+
else:
|
| 383 |
+
# return the metrics instead
|
| 384 |
+
return self._chars[key]
|
| 385 |
+
|
| 386 |
+
def __setitem__(self, key, value):
|
| 387 |
+
if isinstance(key, tuple):
|
| 388 |
+
# key is a tuple, set kernpair
|
| 389 |
+
self._kerning[key] = value
|
| 390 |
+
else:
|
| 391 |
+
# set char metrics
|
| 392 |
+
self._chars[key] = value
|
| 393 |
+
|
| 394 |
+
def __delitem__(self, key):
|
| 395 |
+
if isinstance(key, tuple):
|
| 396 |
+
# key is a tuple, del kernpair
|
| 397 |
+
del self._kerning[key]
|
| 398 |
+
else:
|
| 399 |
+
# del char metrics
|
| 400 |
+
del self._chars[key]
|
| 401 |
+
|
| 402 |
+
def __repr__(self):
|
| 403 |
+
if hasattr(self, "FullName"):
|
| 404 |
+
return "<AFM object for %s>" % self.FullName
|
| 405 |
+
else:
|
| 406 |
+
return "<AFM object at %x>" % id(self)
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def readlines(path):
|
| 410 |
+
with open(path, "r", encoding="ascii") as f:
|
| 411 |
+
data = f.read()
|
| 412 |
+
return data.splitlines()
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def writelines(path, lines, sep="\r"):
|
| 416 |
+
with open(path, "w", encoding="ascii", newline=sep) as f:
|
| 417 |
+
f.write("\n".join(lines) + "\n")
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
if __name__ == "__main__":
|
| 421 |
+
import EasyDialogs
|
| 422 |
+
|
| 423 |
+
path = EasyDialogs.AskFileForOpen()
|
| 424 |
+
if path:
|
| 425 |
+
afm = AFM(path)
|
| 426 |
+
char = "A"
|
| 427 |
+
if afm.has_char(char):
|
| 428 |
+
print(afm[char]) # print charnum, width and boundingbox
|
| 429 |
+
pair = ("A", "V")
|
| 430 |
+
if afm.has_kernpair(pair):
|
| 431 |
+
print(afm[pair]) # print kerning value for pair
|
| 432 |
+
print(afm.Version) # various other afm entries have become attributes
|
| 433 |
+
print(afm.Weight)
|
| 434 |
+
# afm.comments() returns a list of all Comment lines found in the AFM
|
| 435 |
+
print(afm.comments())
|
| 436 |
+
# print afm.chars()
|
| 437 |
+
# print afm.kernpairs()
|
| 438 |
+
print(afm)
|
| 439 |
+
afm.write(path + ".muck")
|
lib/python3.12/site-packages/fontTools/agl.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
lib/python3.12/site-packages/fontTools/annotations.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
from typing import TYPE_CHECKING, Iterable, Optional, TypeVar, Union
|
| 3 |
+
from collections.abc import Callable, Sequence
|
| 4 |
+
from fontTools.misc.filesystem._base import FS
|
| 5 |
+
from os import PathLike
|
| 6 |
+
from xml.etree.ElementTree import Element as ElementTreeElement
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
from fontTools.ufoLib import UFOFormatVersion
|
| 10 |
+
from fontTools.ufoLib.glifLib import GLIFFormatVersion
|
| 11 |
+
from lxml.etree import _Element as LxmlElement
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
T = TypeVar("T") # Generic type
|
| 15 |
+
K = TypeVar("K") # Generic dict key type
|
| 16 |
+
V = TypeVar("V") # Generic dict value type
|
| 17 |
+
|
| 18 |
+
GlyphNameToFileNameFunc = Optional[Callable[[str, set[str]], str]]
|
| 19 |
+
ElementType = Union[ElementTreeElement, "LxmlElement"]
|
| 20 |
+
FormatVersion = Union[int, tuple[int, int]]
|
| 21 |
+
FormatVersions = Optional[Iterable[FormatVersion]]
|
| 22 |
+
GLIFFormatVersionInput = Optional[Union[int, tuple[int, int], "GLIFFormatVersion"]]
|
| 23 |
+
UFOFormatVersionInput = Optional[Union[int, tuple[int, int], "UFOFormatVersion"]]
|
| 24 |
+
IntFloat = Union[int, float]
|
| 25 |
+
KerningPair = tuple[str, str]
|
| 26 |
+
KerningDict = dict[KerningPair, IntFloat]
|
| 27 |
+
KerningGroups = dict[str, Sequence[str]]
|
| 28 |
+
KerningNested = dict[str, dict[str, IntFloat]]
|
| 29 |
+
PathStr = Union[str, PathLike[str]]
|
| 30 |
+
PathOrFS = Union[PathStr, FS]
|
lib/python3.12/site-packages/fontTools/colorLib/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (197 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/builder.cpython-312.pyc
ADDED
|
Binary file (29.9 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/errors.cpython-312.pyc
ADDED
|
Binary file (385 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/geometry.cpython-312.pyc
ADDED
|
Binary file (5.37 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/table_builder.cpython-312.pyc
ADDED
|
Binary file (9.98 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/colorLib/__pycache__/unbuilder.cpython-312.pyc
ADDED
|
Binary file (3.67 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/colorLib/builder.py
ADDED
|
@@ -0,0 +1,664 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
colorLib.builder: Build COLR/CPAL tables from scratch
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import collections
|
| 7 |
+
import copy
|
| 8 |
+
import enum
|
| 9 |
+
from functools import partial
|
| 10 |
+
from math import ceil, log
|
| 11 |
+
from typing import (
|
| 12 |
+
Any,
|
| 13 |
+
Dict,
|
| 14 |
+
Generator,
|
| 15 |
+
Iterable,
|
| 16 |
+
List,
|
| 17 |
+
Mapping,
|
| 18 |
+
Optional,
|
| 19 |
+
Sequence,
|
| 20 |
+
Tuple,
|
| 21 |
+
Type,
|
| 22 |
+
TypeVar,
|
| 23 |
+
Union,
|
| 24 |
+
)
|
| 25 |
+
from fontTools.misc.arrayTools import intRect
|
| 26 |
+
from fontTools.misc.fixedTools import fixedToFloat
|
| 27 |
+
from fontTools.misc.treeTools import build_n_ary_tree
|
| 28 |
+
from fontTools.ttLib.tables import C_O_L_R_
|
| 29 |
+
from fontTools.ttLib.tables import C_P_A_L_
|
| 30 |
+
from fontTools.ttLib.tables import _n_a_m_e
|
| 31 |
+
from fontTools.ttLib.tables import otTables as ot
|
| 32 |
+
from fontTools.ttLib.tables.otTables import ExtendMode, CompositeMode
|
| 33 |
+
from .errors import ColorLibError
|
| 34 |
+
from .geometry import round_start_circle_stable_containment
|
| 35 |
+
from .table_builder import BuildCallback, TableBuilder
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# TODO move type aliases to colorLib.types?
|
| 39 |
+
T = TypeVar("T")
|
| 40 |
+
_Kwargs = Mapping[str, Any]
|
| 41 |
+
_PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
|
| 42 |
+
_PaintInputList = Sequence[_PaintInput]
|
| 43 |
+
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
|
| 44 |
+
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
|
| 45 |
+
_ClipBoxInput = Union[
|
| 46 |
+
Tuple[int, int, int, int, int], # format 1, variable
|
| 47 |
+
Tuple[int, int, int, int], # format 0, non-variable
|
| 48 |
+
ot.ClipBox,
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
MAX_PAINT_COLR_LAYER_COUNT = 255
|
| 53 |
+
_DEFAULT_ALPHA = 1.0
|
| 54 |
+
_MAX_REUSE_LEN = 32
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _beforeBuildPaintRadialGradient(paint, source):
|
| 58 |
+
x0 = source["x0"]
|
| 59 |
+
y0 = source["y0"]
|
| 60 |
+
r0 = source["r0"]
|
| 61 |
+
x1 = source["x1"]
|
| 62 |
+
y1 = source["y1"]
|
| 63 |
+
r1 = source["r1"]
|
| 64 |
+
|
| 65 |
+
# TODO apparently no builder_test confirms this works (?)
|
| 66 |
+
|
| 67 |
+
# avoid abrupt change after rounding when c0 is near c1's perimeter
|
| 68 |
+
c = round_start_circle_stable_containment((x0, y0), r0, (x1, y1), r1)
|
| 69 |
+
x0, y0 = c.centre
|
| 70 |
+
r0 = c.radius
|
| 71 |
+
|
| 72 |
+
# update source to ensure paint is built with corrected values
|
| 73 |
+
source["x0"] = x0
|
| 74 |
+
source["y0"] = y0
|
| 75 |
+
source["r0"] = r0
|
| 76 |
+
source["x1"] = x1
|
| 77 |
+
source["y1"] = y1
|
| 78 |
+
source["r1"] = r1
|
| 79 |
+
|
| 80 |
+
return paint, source
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _defaultColorStop():
|
| 84 |
+
colorStop = ot.ColorStop()
|
| 85 |
+
colorStop.Alpha = _DEFAULT_ALPHA
|
| 86 |
+
return colorStop
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _defaultVarColorStop():
|
| 90 |
+
colorStop = ot.VarColorStop()
|
| 91 |
+
colorStop.Alpha = _DEFAULT_ALPHA
|
| 92 |
+
return colorStop
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _defaultColorLine():
|
| 96 |
+
colorLine = ot.ColorLine()
|
| 97 |
+
colorLine.Extend = ExtendMode.PAD
|
| 98 |
+
return colorLine
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _defaultVarColorLine():
|
| 102 |
+
colorLine = ot.VarColorLine()
|
| 103 |
+
colorLine.Extend = ExtendMode.PAD
|
| 104 |
+
return colorLine
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _defaultPaintSolid():
|
| 108 |
+
paint = ot.Paint()
|
| 109 |
+
paint.Alpha = _DEFAULT_ALPHA
|
| 110 |
+
return paint
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _buildPaintCallbacks():
|
| 114 |
+
return {
|
| 115 |
+
(
|
| 116 |
+
BuildCallback.BEFORE_BUILD,
|
| 117 |
+
ot.Paint,
|
| 118 |
+
ot.PaintFormat.PaintRadialGradient,
|
| 119 |
+
): _beforeBuildPaintRadialGradient,
|
| 120 |
+
(
|
| 121 |
+
BuildCallback.BEFORE_BUILD,
|
| 122 |
+
ot.Paint,
|
| 123 |
+
ot.PaintFormat.PaintVarRadialGradient,
|
| 124 |
+
): _beforeBuildPaintRadialGradient,
|
| 125 |
+
(BuildCallback.CREATE_DEFAULT, ot.ColorStop): _defaultColorStop,
|
| 126 |
+
(BuildCallback.CREATE_DEFAULT, ot.VarColorStop): _defaultVarColorStop,
|
| 127 |
+
(BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
|
| 128 |
+
(BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
|
| 129 |
+
(
|
| 130 |
+
BuildCallback.CREATE_DEFAULT,
|
| 131 |
+
ot.Paint,
|
| 132 |
+
ot.PaintFormat.PaintSolid,
|
| 133 |
+
): _defaultPaintSolid,
|
| 134 |
+
(
|
| 135 |
+
BuildCallback.CREATE_DEFAULT,
|
| 136 |
+
ot.Paint,
|
| 137 |
+
ot.PaintFormat.PaintVarSolid,
|
| 138 |
+
): _defaultPaintSolid,
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def populateCOLRv0(
|
| 143 |
+
table: ot.COLR,
|
| 144 |
+
colorGlyphsV0: _ColorGlyphsV0Dict,
|
| 145 |
+
glyphMap: Optional[Mapping[str, int]] = None,
|
| 146 |
+
):
|
| 147 |
+
"""Build v0 color layers and add to existing COLR table.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
table: a raw ``otTables.COLR()`` object (not ttLib's ``table_C_O_L_R_``).
|
| 151 |
+
colorGlyphsV0: map of base glyph names to lists of (layer glyph names,
|
| 152 |
+
color palette index) tuples. Can be empty.
|
| 153 |
+
glyphMap: a map from glyph names to glyph indices, as returned from
|
| 154 |
+
``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID.
|
| 155 |
+
"""
|
| 156 |
+
if glyphMap is not None:
|
| 157 |
+
colorGlyphItems = sorted(
|
| 158 |
+
colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
|
| 159 |
+
)
|
| 160 |
+
else:
|
| 161 |
+
colorGlyphItems = colorGlyphsV0.items()
|
| 162 |
+
baseGlyphRecords = []
|
| 163 |
+
layerRecords = []
|
| 164 |
+
for baseGlyph, layers in colorGlyphItems:
|
| 165 |
+
baseRec = ot.BaseGlyphRecord()
|
| 166 |
+
baseRec.BaseGlyph = baseGlyph
|
| 167 |
+
baseRec.FirstLayerIndex = len(layerRecords)
|
| 168 |
+
baseRec.NumLayers = len(layers)
|
| 169 |
+
baseGlyphRecords.append(baseRec)
|
| 170 |
+
|
| 171 |
+
for layerGlyph, paletteIndex in layers:
|
| 172 |
+
layerRec = ot.LayerRecord()
|
| 173 |
+
layerRec.LayerGlyph = layerGlyph
|
| 174 |
+
layerRec.PaletteIndex = paletteIndex
|
| 175 |
+
layerRecords.append(layerRec)
|
| 176 |
+
|
| 177 |
+
table.BaseGlyphRecordArray = table.LayerRecordArray = None
|
| 178 |
+
if baseGlyphRecords:
|
| 179 |
+
table.BaseGlyphRecordArray = ot.BaseGlyphRecordArray()
|
| 180 |
+
table.BaseGlyphRecordArray.BaseGlyphRecord = baseGlyphRecords
|
| 181 |
+
if layerRecords:
|
| 182 |
+
table.LayerRecordArray = ot.LayerRecordArray()
|
| 183 |
+
table.LayerRecordArray.LayerRecord = layerRecords
|
| 184 |
+
table.BaseGlyphRecordCount = len(baseGlyphRecords)
|
| 185 |
+
table.LayerRecordCount = len(layerRecords)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def buildCOLR(
|
| 189 |
+
colorGlyphs: _ColorGlyphsDict,
|
| 190 |
+
version: Optional[int] = None,
|
| 191 |
+
*,
|
| 192 |
+
glyphMap: Optional[Mapping[str, int]] = None,
|
| 193 |
+
varStore: Optional[ot.VarStore] = None,
|
| 194 |
+
varIndexMap: Optional[ot.DeltaSetIndexMap] = None,
|
| 195 |
+
clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None,
|
| 196 |
+
allowLayerReuse: bool = True,
|
| 197 |
+
) -> C_O_L_R_.table_C_O_L_R_:
|
| 198 |
+
"""Build COLR table from color layers mapping.
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
|
| 202 |
+
colorGlyphs: map of base glyph name to, either list of (layer glyph name,
|
| 203 |
+
color palette index) tuples for COLRv0; or a single ``Paint`` (dict) or
|
| 204 |
+
list of ``Paint`` for COLRv1.
|
| 205 |
+
version: the version of COLR table. If None, the version is determined
|
| 206 |
+
by the presence of COLRv1 paints or variation data (varStore), which
|
| 207 |
+
require version 1; otherwise, if all base glyphs use only simple color
|
| 208 |
+
layers, version 0 is used.
|
| 209 |
+
glyphMap: a map from glyph names to glyph indices, as returned from
|
| 210 |
+
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
| 211 |
+
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
|
| 212 |
+
varIndexMap: Optional DeltaSetIndexMap for deltas associated with v1 layer.
|
| 213 |
+
clipBoxes: Optional map of base glyph name to clip box 4- or 5-tuples:
|
| 214 |
+
(xMin, yMin, xMax, yMax) or (xMin, yMin, xMax, yMax, varIndexBase).
|
| 215 |
+
|
| 216 |
+
Returns:
|
| 217 |
+
A new COLR table.
|
| 218 |
+
"""
|
| 219 |
+
self = C_O_L_R_.table_C_O_L_R_()
|
| 220 |
+
|
| 221 |
+
if varStore is not None and version == 0:
|
| 222 |
+
raise ValueError("Can't add VarStore to COLRv0")
|
| 223 |
+
|
| 224 |
+
if version in (None, 0) and not varStore:
|
| 225 |
+
# split color glyphs into v0 and v1 and encode separately
|
| 226 |
+
colorGlyphsV0, colorGlyphsV1 = _split_color_glyphs_by_version(colorGlyphs)
|
| 227 |
+
if version == 0 and colorGlyphsV1:
|
| 228 |
+
raise ValueError("Can't encode COLRv1 glyphs in COLRv0")
|
| 229 |
+
else:
|
| 230 |
+
# unless explicitly requested for v1 or have variations, in which case
|
| 231 |
+
# we encode all color glyph as v1
|
| 232 |
+
colorGlyphsV0, colorGlyphsV1 = {}, colorGlyphs
|
| 233 |
+
|
| 234 |
+
colr = ot.COLR()
|
| 235 |
+
|
| 236 |
+
populateCOLRv0(colr, colorGlyphsV0, glyphMap)
|
| 237 |
+
|
| 238 |
+
colr.LayerList, colr.BaseGlyphList = buildColrV1(
|
| 239 |
+
colorGlyphsV1,
|
| 240 |
+
glyphMap,
|
| 241 |
+
allowLayerReuse=allowLayerReuse,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
if version is None:
|
| 245 |
+
version = 1 if (varStore or colorGlyphsV1) else 0
|
| 246 |
+
elif version not in (0, 1):
|
| 247 |
+
raise NotImplementedError(version)
|
| 248 |
+
self.version = colr.Version = version
|
| 249 |
+
|
| 250 |
+
if version == 0:
|
| 251 |
+
self.ColorLayers = self._decompileColorLayersV0(colr)
|
| 252 |
+
else:
|
| 253 |
+
colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None
|
| 254 |
+
colr.VarIndexMap = varIndexMap
|
| 255 |
+
colr.VarStore = varStore
|
| 256 |
+
self.table = colr
|
| 257 |
+
|
| 258 |
+
return self
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def buildClipList(clipBoxes: Dict[str, _ClipBoxInput]) -> ot.ClipList:
|
| 262 |
+
clipList = ot.ClipList()
|
| 263 |
+
clipList.Format = 1
|
| 264 |
+
clipList.clips = {name: buildClipBox(box) for name, box in clipBoxes.items()}
|
| 265 |
+
return clipList
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def buildClipBox(clipBox: _ClipBoxInput) -> ot.ClipBox:
|
| 269 |
+
if isinstance(clipBox, ot.ClipBox):
|
| 270 |
+
return clipBox
|
| 271 |
+
n = len(clipBox)
|
| 272 |
+
clip = ot.ClipBox()
|
| 273 |
+
if n not in (4, 5):
|
| 274 |
+
raise ValueError(f"Invalid ClipBox: expected 4 or 5 values, found {n}")
|
| 275 |
+
clip.xMin, clip.yMin, clip.xMax, clip.yMax = intRect(clipBox[:4])
|
| 276 |
+
clip.Format = int(n == 5) + 1
|
| 277 |
+
if n == 5:
|
| 278 |
+
clip.VarIndexBase = int(clipBox[4])
|
| 279 |
+
return clip
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
class ColorPaletteType(enum.IntFlag):
|
| 283 |
+
USABLE_WITH_LIGHT_BACKGROUND = 0x0001
|
| 284 |
+
USABLE_WITH_DARK_BACKGROUND = 0x0002
|
| 285 |
+
|
| 286 |
+
@classmethod
|
| 287 |
+
def _missing_(cls, value):
|
| 288 |
+
# enforce reserved bits
|
| 289 |
+
if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0):
|
| 290 |
+
raise ValueError(f"{value} is not a valid {cls.__name__}")
|
| 291 |
+
return super()._missing_(value)
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
# None, 'abc' or {'en': 'abc', 'de': 'xyz'}
|
| 295 |
+
_OptionalLocalizedString = Union[None, str, Dict[str, str]]
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
def buildPaletteLabels(
|
| 299 |
+
labels: Iterable[_OptionalLocalizedString], nameTable: _n_a_m_e.table__n_a_m_e
|
| 300 |
+
) -> List[Optional[int]]:
|
| 301 |
+
return [
|
| 302 |
+
(
|
| 303 |
+
nameTable.addMultilingualName(l, mac=False)
|
| 304 |
+
if isinstance(l, dict)
|
| 305 |
+
else (
|
| 306 |
+
C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
| 307 |
+
if l is None
|
| 308 |
+
else nameTable.addMultilingualName({"en": l}, mac=False)
|
| 309 |
+
)
|
| 310 |
+
)
|
| 311 |
+
for l in labels
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def buildCPAL(
|
| 316 |
+
palettes: Sequence[Sequence[Tuple[float, float, float, float]]],
|
| 317 |
+
paletteTypes: Optional[Sequence[ColorPaletteType]] = None,
|
| 318 |
+
paletteLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
| 319 |
+
paletteEntryLabels: Optional[Sequence[_OptionalLocalizedString]] = None,
|
| 320 |
+
nameTable: Optional[_n_a_m_e.table__n_a_m_e] = None,
|
| 321 |
+
) -> C_P_A_L_.table_C_P_A_L_:
|
| 322 |
+
"""Build CPAL table from list of color palettes.
|
| 323 |
+
|
| 324 |
+
Args:
|
| 325 |
+
palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats
|
| 326 |
+
in the range [0..1].
|
| 327 |
+
paletteTypes: optional list of ColorPaletteType, one for each palette.
|
| 328 |
+
paletteLabels: optional list of palette labels. Each lable can be either:
|
| 329 |
+
None (no label), a string (for for default English labels), or a
|
| 330 |
+
localized string (as a dict keyed with BCP47 language codes).
|
| 331 |
+
paletteEntryLabels: optional list of palette entry labels, one for each
|
| 332 |
+
palette entry (see paletteLabels).
|
| 333 |
+
nameTable: optional name table where to store palette and palette entry
|
| 334 |
+
labels. Required if either paletteLabels or paletteEntryLabels is set.
|
| 335 |
+
|
| 336 |
+
Return:
|
| 337 |
+
A new CPAL v0 or v1 table, if custom palette types or labels are specified.
|
| 338 |
+
"""
|
| 339 |
+
if len({len(p) for p in palettes}) != 1:
|
| 340 |
+
raise ColorLibError("color palettes have different lengths")
|
| 341 |
+
|
| 342 |
+
if (paletteLabels or paletteEntryLabels) and not nameTable:
|
| 343 |
+
raise TypeError(
|
| 344 |
+
"nameTable is required if palette or palette entries have labels"
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
cpal = C_P_A_L_.table_C_P_A_L_()
|
| 348 |
+
cpal.numPaletteEntries = len(palettes[0])
|
| 349 |
+
|
| 350 |
+
cpal.palettes = []
|
| 351 |
+
for i, palette in enumerate(palettes):
|
| 352 |
+
colors = []
|
| 353 |
+
for j, color in enumerate(palette):
|
| 354 |
+
if not isinstance(color, tuple) or len(color) != 4:
|
| 355 |
+
raise ColorLibError(
|
| 356 |
+
f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}"
|
| 357 |
+
)
|
| 358 |
+
if any(v > 1 or v < 0 for v in color):
|
| 359 |
+
raise ColorLibError(
|
| 360 |
+
f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}"
|
| 361 |
+
)
|
| 362 |
+
# input colors are RGBA, CPAL encodes them as BGRA
|
| 363 |
+
red, green, blue, alpha = color
|
| 364 |
+
colors.append(
|
| 365 |
+
C_P_A_L_.Color(*(round(v * 255) for v in (blue, green, red, alpha)))
|
| 366 |
+
)
|
| 367 |
+
cpal.palettes.append(colors)
|
| 368 |
+
|
| 369 |
+
if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
|
| 370 |
+
cpal.version = 1
|
| 371 |
+
|
| 372 |
+
if paletteTypes is not None:
|
| 373 |
+
if len(paletteTypes) != len(palettes):
|
| 374 |
+
raise ColorLibError(
|
| 375 |
+
f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}"
|
| 376 |
+
)
|
| 377 |
+
cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
|
| 378 |
+
else:
|
| 379 |
+
cpal.paletteTypes = [C_P_A_L_.table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(
|
| 380 |
+
palettes
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
if paletteLabels is not None:
|
| 384 |
+
if len(paletteLabels) != len(palettes):
|
| 385 |
+
raise ColorLibError(
|
| 386 |
+
f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}"
|
| 387 |
+
)
|
| 388 |
+
cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
|
| 389 |
+
else:
|
| 390 |
+
cpal.paletteLabels = [C_P_A_L_.table_C_P_A_L_.NO_NAME_ID] * len(palettes)
|
| 391 |
+
|
| 392 |
+
if paletteEntryLabels is not None:
|
| 393 |
+
if len(paletteEntryLabels) != cpal.numPaletteEntries:
|
| 394 |
+
raise ColorLibError(
|
| 395 |
+
f"Expected {cpal.numPaletteEntries} paletteEntryLabels, "
|
| 396 |
+
f"got {len(paletteEntryLabels)}"
|
| 397 |
+
)
|
| 398 |
+
cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
|
| 399 |
+
else:
|
| 400 |
+
cpal.paletteEntryLabels = [
|
| 401 |
+
C_P_A_L_.table_C_P_A_L_.NO_NAME_ID
|
| 402 |
+
] * cpal.numPaletteEntries
|
| 403 |
+
else:
|
| 404 |
+
cpal.version = 0
|
| 405 |
+
|
| 406 |
+
return cpal
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
# COLR v1 tables
|
| 410 |
+
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
|
| 411 |
+
|
| 412 |
+
|
| 413 |
+
def _is_colrv0_layer(layer: Any) -> bool:
|
| 414 |
+
# Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
|
| 415 |
+
# the first element is a str (the layerGlyph) and the second element is an int
|
| 416 |
+
# (CPAL paletteIndex).
|
| 417 |
+
# https://github.com/googlefonts/ufo2ft/issues/426
|
| 418 |
+
try:
|
| 419 |
+
layerGlyph, paletteIndex = layer
|
| 420 |
+
except (TypeError, ValueError):
|
| 421 |
+
return False
|
| 422 |
+
else:
|
| 423 |
+
return isinstance(layerGlyph, str) and isinstance(paletteIndex, int)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _split_color_glyphs_by_version(
|
| 427 |
+
colorGlyphs: _ColorGlyphsDict,
|
| 428 |
+
) -> Tuple[_ColorGlyphsV0Dict, _ColorGlyphsDict]:
|
| 429 |
+
colorGlyphsV0 = {}
|
| 430 |
+
colorGlyphsV1 = {}
|
| 431 |
+
for baseGlyph, layers in colorGlyphs.items():
|
| 432 |
+
if all(_is_colrv0_layer(l) for l in layers):
|
| 433 |
+
colorGlyphsV0[baseGlyph] = layers
|
| 434 |
+
else:
|
| 435 |
+
colorGlyphsV1[baseGlyph] = layers
|
| 436 |
+
|
| 437 |
+
# sanity check
|
| 438 |
+
assert set(colorGlyphs) == (set(colorGlyphsV0) | set(colorGlyphsV1))
|
| 439 |
+
|
| 440 |
+
return colorGlyphsV0, colorGlyphsV1
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
|
| 444 |
+
# TODO feels like something itertools might have already
|
| 445 |
+
for lbound in range(num_layers):
|
| 446 |
+
# Reuse of very large #s of layers is relatively unlikely
|
| 447 |
+
# +2: we want sequences of at least 2
|
| 448 |
+
# otData handles single-record duplication
|
| 449 |
+
for ubound in range(
|
| 450 |
+
lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
|
| 451 |
+
):
|
| 452 |
+
yield (lbound, ubound)
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
class LayerReuseCache:
|
| 456 |
+
reusePool: Mapping[Tuple[Any, ...], int]
|
| 457 |
+
tuples: Mapping[int, Tuple[Any, ...]]
|
| 458 |
+
keepAlive: List[ot.Paint] # we need id to remain valid
|
| 459 |
+
|
| 460 |
+
def __init__(self):
|
| 461 |
+
self.reusePool = {}
|
| 462 |
+
self.tuples = {}
|
| 463 |
+
self.keepAlive = []
|
| 464 |
+
|
| 465 |
+
def _paint_tuple(self, paint: ot.Paint):
|
| 466 |
+
# start simple, who even cares about cyclic graphs or interesting field types
|
| 467 |
+
def _tuple_safe(value):
|
| 468 |
+
if isinstance(value, enum.Enum):
|
| 469 |
+
return value
|
| 470 |
+
elif hasattr(value, "__dict__"):
|
| 471 |
+
return tuple(
|
| 472 |
+
(k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items())
|
| 473 |
+
)
|
| 474 |
+
elif isinstance(value, collections.abc.MutableSequence):
|
| 475 |
+
return tuple(_tuple_safe(e) for e in value)
|
| 476 |
+
return value
|
| 477 |
+
|
| 478 |
+
# Cache the tuples for individual Paint instead of the whole sequence
|
| 479 |
+
# because the seq could be a transient slice
|
| 480 |
+
result = self.tuples.get(id(paint), None)
|
| 481 |
+
if result is None:
|
| 482 |
+
result = _tuple_safe(paint)
|
| 483 |
+
self.tuples[id(paint)] = result
|
| 484 |
+
self.keepAlive.append(paint)
|
| 485 |
+
return result
|
| 486 |
+
|
| 487 |
+
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
|
| 488 |
+
return tuple(self._paint_tuple(p) for p in paints)
|
| 489 |
+
|
| 490 |
+
def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
|
| 491 |
+
found_reuse = True
|
| 492 |
+
while found_reuse:
|
| 493 |
+
found_reuse = False
|
| 494 |
+
|
| 495 |
+
ranges = sorted(
|
| 496 |
+
_reuse_ranges(len(layers)),
|
| 497 |
+
key=lambda t: (t[1] - t[0], t[1], t[0]),
|
| 498 |
+
reverse=True,
|
| 499 |
+
)
|
| 500 |
+
for lbound, ubound in ranges:
|
| 501 |
+
reuse_lbound = self.reusePool.get(
|
| 502 |
+
self._as_tuple(layers[lbound:ubound]), -1
|
| 503 |
+
)
|
| 504 |
+
if reuse_lbound == -1:
|
| 505 |
+
continue
|
| 506 |
+
new_slice = ot.Paint()
|
| 507 |
+
new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
|
| 508 |
+
new_slice.NumLayers = ubound - lbound
|
| 509 |
+
new_slice.FirstLayerIndex = reuse_lbound
|
| 510 |
+
layers = layers[:lbound] + [new_slice] + layers[ubound:]
|
| 511 |
+
found_reuse = True
|
| 512 |
+
break
|
| 513 |
+
return layers
|
| 514 |
+
|
| 515 |
+
def add(self, layers: List[ot.Paint], first_layer_index: int):
|
| 516 |
+
for lbound, ubound in _reuse_ranges(len(layers)):
|
| 517 |
+
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
|
| 518 |
+
lbound + first_layer_index
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
class LayerListBuilder:
|
| 523 |
+
layers: List[ot.Paint]
|
| 524 |
+
cache: LayerReuseCache
|
| 525 |
+
allowLayerReuse: bool
|
| 526 |
+
|
| 527 |
+
def __init__(self, *, allowLayerReuse=True):
|
| 528 |
+
self.layers = []
|
| 529 |
+
if allowLayerReuse:
|
| 530 |
+
self.cache = LayerReuseCache()
|
| 531 |
+
else:
|
| 532 |
+
self.cache = None
|
| 533 |
+
|
| 534 |
+
# We need to intercept construction of PaintColrLayers
|
| 535 |
+
callbacks = _buildPaintCallbacks()
|
| 536 |
+
callbacks[
|
| 537 |
+
(
|
| 538 |
+
BuildCallback.BEFORE_BUILD,
|
| 539 |
+
ot.Paint,
|
| 540 |
+
ot.PaintFormat.PaintColrLayers,
|
| 541 |
+
)
|
| 542 |
+
] = self._beforeBuildPaintColrLayers
|
| 543 |
+
self.tableBuilder = TableBuilder(callbacks)
|
| 544 |
+
|
| 545 |
+
# COLR layers is unusual in that it modifies shared state
|
| 546 |
+
# so we need a callback into an object
|
| 547 |
+
def _beforeBuildPaintColrLayers(self, dest, source):
|
| 548 |
+
# Sketchy gymnastics: a sequence input will have dropped it's layers
|
| 549 |
+
# into NumLayers; get it back
|
| 550 |
+
if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
|
| 551 |
+
layers = source["NumLayers"]
|
| 552 |
+
else:
|
| 553 |
+
layers = source["Layers"]
|
| 554 |
+
|
| 555 |
+
# Convert maps seqs or whatever into typed objects
|
| 556 |
+
layers = [self.buildPaint(l) for l in layers]
|
| 557 |
+
|
| 558 |
+
# No reason to have a colr layers with just one entry
|
| 559 |
+
if len(layers) == 1:
|
| 560 |
+
return layers[0], {}
|
| 561 |
+
|
| 562 |
+
if self.cache is not None:
|
| 563 |
+
# Look for reuse, with preference to longer sequences
|
| 564 |
+
# This may make the layer list smaller
|
| 565 |
+
layers = self.cache.try_reuse(layers)
|
| 566 |
+
|
| 567 |
+
# The layer list is now final; if it's too big we need to tree it
|
| 568 |
+
is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
|
| 569 |
+
layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
|
| 570 |
+
|
| 571 |
+
# We now have a tree of sequences with Paint leaves.
|
| 572 |
+
# Convert the sequences into PaintColrLayers.
|
| 573 |
+
def listToColrLayers(layer):
|
| 574 |
+
if isinstance(layer, collections.abc.Sequence):
|
| 575 |
+
return self.buildPaint(
|
| 576 |
+
{
|
| 577 |
+
"Format": ot.PaintFormat.PaintColrLayers,
|
| 578 |
+
"Layers": [listToColrLayers(l) for l in layer],
|
| 579 |
+
}
|
| 580 |
+
)
|
| 581 |
+
return layer
|
| 582 |
+
|
| 583 |
+
layers = [listToColrLayers(l) for l in layers]
|
| 584 |
+
|
| 585 |
+
# No reason to have a colr layers with just one entry
|
| 586 |
+
if len(layers) == 1:
|
| 587 |
+
return layers[0], {}
|
| 588 |
+
|
| 589 |
+
paint = ot.Paint()
|
| 590 |
+
paint.Format = int(ot.PaintFormat.PaintColrLayers)
|
| 591 |
+
paint.NumLayers = len(layers)
|
| 592 |
+
paint.FirstLayerIndex = len(self.layers)
|
| 593 |
+
self.layers.extend(layers)
|
| 594 |
+
|
| 595 |
+
# Register our parts for reuse provided we aren't a tree
|
| 596 |
+
# If we are a tree the leaves registered for reuse and that will suffice
|
| 597 |
+
if self.cache is not None and not is_tree:
|
| 598 |
+
self.cache.add(layers, paint.FirstLayerIndex)
|
| 599 |
+
|
| 600 |
+
# we've fully built dest; empty source prevents generalized build from kicking in
|
| 601 |
+
return paint, {}
|
| 602 |
+
|
| 603 |
+
def buildPaint(self, paint: _PaintInput) -> ot.Paint:
|
| 604 |
+
return self.tableBuilder.build(ot.Paint, paint)
|
| 605 |
+
|
| 606 |
+
def build(self) -> Optional[ot.LayerList]:
|
| 607 |
+
if not self.layers:
|
| 608 |
+
return None
|
| 609 |
+
layers = ot.LayerList()
|
| 610 |
+
layers.LayerCount = len(self.layers)
|
| 611 |
+
layers.Paint = self.layers
|
| 612 |
+
return layers
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
def buildBaseGlyphPaintRecord(
|
| 616 |
+
baseGlyph: str, layerBuilder: LayerListBuilder, paint: _PaintInput
|
| 617 |
+
) -> ot.BaseGlyphList:
|
| 618 |
+
self = ot.BaseGlyphPaintRecord()
|
| 619 |
+
self.BaseGlyph = baseGlyph
|
| 620 |
+
self.Paint = layerBuilder.buildPaint(paint)
|
| 621 |
+
return self
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
|
| 625 |
+
lines = []
|
| 626 |
+
for baseGlyph, error in sorted(errors.items()):
|
| 627 |
+
lines.append(f" {baseGlyph} => {type(error).__name__}: {error}")
|
| 628 |
+
return "\n".join(lines)
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
def buildColrV1(
|
| 632 |
+
colorGlyphs: _ColorGlyphsDict,
|
| 633 |
+
glyphMap: Optional[Mapping[str, int]] = None,
|
| 634 |
+
*,
|
| 635 |
+
allowLayerReuse: bool = True,
|
| 636 |
+
) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
|
| 637 |
+
if glyphMap is not None:
|
| 638 |
+
colorGlyphItems = sorted(
|
| 639 |
+
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
|
| 640 |
+
)
|
| 641 |
+
else:
|
| 642 |
+
colorGlyphItems = colorGlyphs.items()
|
| 643 |
+
|
| 644 |
+
errors = {}
|
| 645 |
+
baseGlyphs = []
|
| 646 |
+
layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
|
| 647 |
+
for baseGlyph, paint in colorGlyphItems:
|
| 648 |
+
try:
|
| 649 |
+
baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
|
| 650 |
+
|
| 651 |
+
except (ColorLibError, OverflowError, ValueError, TypeError) as e:
|
| 652 |
+
errors[baseGlyph] = e
|
| 653 |
+
|
| 654 |
+
if errors:
|
| 655 |
+
failed_glyphs = _format_glyph_errors(errors)
|
| 656 |
+
exc = ColorLibError(f"Failed to build BaseGlyphList:\n{failed_glyphs}")
|
| 657 |
+
exc.errors = errors
|
| 658 |
+
raise exc from next(iter(errors.values()))
|
| 659 |
+
|
| 660 |
+
layers = layerBuilder.build()
|
| 661 |
+
glyphs = ot.BaseGlyphList()
|
| 662 |
+
glyphs.BaseGlyphCount = len(baseGlyphs)
|
| 663 |
+
glyphs.BaseGlyphPaintRecord = baseGlyphs
|
| 664 |
+
return (layers, glyphs)
|
lib/python3.12/site-packages/fontTools/colorLib/errors.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ColorLibError(Exception):
|
| 2 |
+
pass
|
lib/python3.12/site-packages/fontTools/colorLib/geometry.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Helpers for manipulating 2D points and vectors in COLR table."""
|
| 2 |
+
|
| 3 |
+
from math import copysign, cos, hypot, isclose, pi
|
| 4 |
+
from fontTools.misc.roundTools import otRound
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _vector_between(origin, target):
|
| 8 |
+
return (target[0] - origin[0], target[1] - origin[1])
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _round_point(pt):
|
| 12 |
+
return (otRound(pt[0]), otRound(pt[1]))
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def _unit_vector(vec):
|
| 16 |
+
length = hypot(*vec)
|
| 17 |
+
if length == 0:
|
| 18 |
+
return None
|
| 19 |
+
return (vec[0] / length, vec[1] / length)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
_CIRCLE_INSIDE_TOLERANCE = 1e-4
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# The unit vector's X and Y components are respectively
|
| 26 |
+
# U = (cos(α), sin(α))
|
| 27 |
+
# where α is the angle between the unit vector and the positive x axis.
|
| 28 |
+
_UNIT_VECTOR_THRESHOLD = cos(3 / 8 * pi) # == sin(1/8 * pi) == 0.38268343236508984
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _rounding_offset(direction):
|
| 32 |
+
# Return 2-tuple of -/+ 1.0 or 0.0 approximately based on the direction vector.
|
| 33 |
+
# We divide the unit circle in 8 equal slices oriented towards the cardinal
|
| 34 |
+
# (N, E, S, W) and intermediate (NE, SE, SW, NW) directions. To each slice we
|
| 35 |
+
# map one of the possible cases: -1, 0, +1 for either X and Y coordinate.
|
| 36 |
+
# E.g. Return (+1.0, -1.0) if unit vector is oriented towards SE, or
|
| 37 |
+
# (-1.0, 0.0) if it's pointing West, etc.
|
| 38 |
+
uv = _unit_vector(direction)
|
| 39 |
+
if not uv:
|
| 40 |
+
return (0, 0)
|
| 41 |
+
|
| 42 |
+
result = []
|
| 43 |
+
for uv_component in uv:
|
| 44 |
+
if -_UNIT_VECTOR_THRESHOLD <= uv_component < _UNIT_VECTOR_THRESHOLD:
|
| 45 |
+
# unit vector component near 0: direction almost orthogonal to the
|
| 46 |
+
# direction of the current axis, thus keep coordinate unchanged
|
| 47 |
+
result.append(0)
|
| 48 |
+
else:
|
| 49 |
+
# nudge coord by +/- 1.0 in direction of unit vector
|
| 50 |
+
result.append(copysign(1.0, uv_component))
|
| 51 |
+
return tuple(result)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class Circle:
|
| 55 |
+
def __init__(self, centre, radius):
|
| 56 |
+
self.centre = centre
|
| 57 |
+
self.radius = radius
|
| 58 |
+
|
| 59 |
+
def __repr__(self):
|
| 60 |
+
return f"Circle(centre={self.centre}, radius={self.radius})"
|
| 61 |
+
|
| 62 |
+
def round(self):
|
| 63 |
+
return Circle(_round_point(self.centre), otRound(self.radius))
|
| 64 |
+
|
| 65 |
+
def inside(self, outer_circle, tolerance=_CIRCLE_INSIDE_TOLERANCE):
|
| 66 |
+
dist = self.radius + hypot(*_vector_between(self.centre, outer_circle.centre))
|
| 67 |
+
return (
|
| 68 |
+
isclose(outer_circle.radius, dist, rel_tol=_CIRCLE_INSIDE_TOLERANCE)
|
| 69 |
+
or outer_circle.radius > dist
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
def concentric(self, other):
|
| 73 |
+
return self.centre == other.centre
|
| 74 |
+
|
| 75 |
+
def move(self, dx, dy):
|
| 76 |
+
self.centre = (self.centre[0] + dx, self.centre[1] + dy)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def round_start_circle_stable_containment(c0, r0, c1, r1):
|
| 80 |
+
"""Round start circle so that it stays inside/outside end circle after rounding.
|
| 81 |
+
|
| 82 |
+
The rounding of circle coordinates to integers may cause an abrupt change
|
| 83 |
+
if the start circle c0 is so close to the end circle c1's perimiter that
|
| 84 |
+
it ends up falling outside (or inside) as a result of the rounding.
|
| 85 |
+
To keep the gradient unchanged, we nudge it in the right direction.
|
| 86 |
+
|
| 87 |
+
See:
|
| 88 |
+
https://github.com/googlefonts/colr-gradients-spec/issues/204
|
| 89 |
+
https://github.com/googlefonts/picosvg/issues/158
|
| 90 |
+
"""
|
| 91 |
+
start, end = Circle(c0, r0), Circle(c1, r1)
|
| 92 |
+
|
| 93 |
+
inside_before_round = start.inside(end)
|
| 94 |
+
|
| 95 |
+
round_start = start.round()
|
| 96 |
+
round_end = end.round()
|
| 97 |
+
inside_after_round = round_start.inside(round_end)
|
| 98 |
+
|
| 99 |
+
if inside_before_round == inside_after_round:
|
| 100 |
+
return round_start
|
| 101 |
+
elif inside_after_round:
|
| 102 |
+
# start was outside before rounding: we need to push start away from end
|
| 103 |
+
direction = _vector_between(round_end.centre, round_start.centre)
|
| 104 |
+
radius_delta = +1.0
|
| 105 |
+
else:
|
| 106 |
+
# start was inside before rounding: we need to push start towards end
|
| 107 |
+
direction = _vector_between(round_start.centre, round_end.centre)
|
| 108 |
+
radius_delta = -1.0
|
| 109 |
+
dx, dy = _rounding_offset(direction)
|
| 110 |
+
|
| 111 |
+
# At most 2 iterations ought to be enough to converge. Before the loop, we
|
| 112 |
+
# know the start circle didn't keep containment after normal rounding; thus
|
| 113 |
+
# we continue adjusting by -/+ 1.0 until containment is restored.
|
| 114 |
+
# Normal rounding can at most move each coordinates -/+0.5; in the worst case
|
| 115 |
+
# both the start and end circle's centres and radii will be rounded in opposite
|
| 116 |
+
# directions, e.g. when they move along a 45 degree diagonal:
|
| 117 |
+
# c0 = (1.5, 1.5) ===> (2.0, 2.0)
|
| 118 |
+
# r0 = 0.5 ===> 1.0
|
| 119 |
+
# c1 = (0.499, 0.499) ===> (0.0, 0.0)
|
| 120 |
+
# r1 = 2.499 ===> 2.0
|
| 121 |
+
# In this example, the relative distance between the circles, calculated
|
| 122 |
+
# as r1 - (r0 + distance(c0, c1)) is initially 0.57437 (c0 is inside c1), and
|
| 123 |
+
# -1.82842 after rounding (c0 is now outside c1). Nudging c0 by -1.0 on both
|
| 124 |
+
# x and y axes moves it towards c1 by hypot(-1.0, -1.0) = 1.41421. Two of these
|
| 125 |
+
# moves cover twice that distance, which is enough to restore containment.
|
| 126 |
+
max_attempts = 2
|
| 127 |
+
for _ in range(max_attempts):
|
| 128 |
+
if round_start.concentric(round_end):
|
| 129 |
+
# can't move c0 towards c1 (they are the same), so we change the radius
|
| 130 |
+
round_start.radius += radius_delta
|
| 131 |
+
assert round_start.radius >= 0
|
| 132 |
+
else:
|
| 133 |
+
round_start.move(dx, dy)
|
| 134 |
+
if inside_before_round == round_start.inside(round_end):
|
| 135 |
+
break
|
| 136 |
+
else: # likely a bug
|
| 137 |
+
raise AssertionError(
|
| 138 |
+
f"Rounding circle {start} "
|
| 139 |
+
f"{'inside' if inside_before_round else 'outside'} "
|
| 140 |
+
f"{end} failed after {max_attempts} attempts!"
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
return round_start
|
lib/python3.12/site-packages/fontTools/colorLib/table_builder.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such.
|
| 3 |
+
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import collections
|
| 7 |
+
import enum
|
| 8 |
+
from fontTools.ttLib.tables.otBase import (
|
| 9 |
+
BaseTable,
|
| 10 |
+
FormatSwitchingBaseTable,
|
| 11 |
+
UInt8FormatSwitchingBaseTable,
|
| 12 |
+
)
|
| 13 |
+
from fontTools.ttLib.tables.otConverters import (
|
| 14 |
+
ComputedInt,
|
| 15 |
+
SimpleValue,
|
| 16 |
+
Struct,
|
| 17 |
+
Short,
|
| 18 |
+
UInt8,
|
| 19 |
+
UShort,
|
| 20 |
+
IntValue,
|
| 21 |
+
FloatValue,
|
| 22 |
+
OptionalValue,
|
| 23 |
+
)
|
| 24 |
+
from fontTools.misc.roundTools import otRound
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class BuildCallback(enum.Enum):
|
| 28 |
+
"""Keyed on (BEFORE_BUILD, class[, Format if available]).
|
| 29 |
+
Receives (dest, source).
|
| 30 |
+
Should return (dest, source), which can be new objects.
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
BEFORE_BUILD = enum.auto()
|
| 34 |
+
|
| 35 |
+
"""Keyed on (AFTER_BUILD, class[, Format if available]).
|
| 36 |
+
Receives (dest).
|
| 37 |
+
Should return dest, which can be a new object.
|
| 38 |
+
"""
|
| 39 |
+
AFTER_BUILD = enum.auto()
|
| 40 |
+
|
| 41 |
+
"""Keyed on (CREATE_DEFAULT, class[, Format if available]).
|
| 42 |
+
Receives no arguments.
|
| 43 |
+
Should return a new instance of class.
|
| 44 |
+
"""
|
| 45 |
+
CREATE_DEFAULT = enum.auto()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _assignable(convertersByName):
|
| 49 |
+
return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _isNonStrSequence(value):
|
| 53 |
+
return isinstance(value, collections.abc.Sequence) and not isinstance(value, str)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _split_format(cls, source):
|
| 57 |
+
if _isNonStrSequence(source):
|
| 58 |
+
assert len(source) > 0, f"{cls} needs at least format from {source}"
|
| 59 |
+
fmt, remainder = source[0], source[1:]
|
| 60 |
+
elif isinstance(source, collections.abc.Mapping):
|
| 61 |
+
assert "Format" in source, f"{cls} needs at least Format from {source}"
|
| 62 |
+
remainder = source.copy()
|
| 63 |
+
fmt = remainder.pop("Format")
|
| 64 |
+
else:
|
| 65 |
+
raise ValueError(f"Not sure how to populate {cls} from {source}")
|
| 66 |
+
|
| 67 |
+
assert isinstance(
|
| 68 |
+
fmt, collections.abc.Hashable
|
| 69 |
+
), f"{cls} Format is not hashable: {fmt!r}"
|
| 70 |
+
assert fmt in cls.convertersByName, f"{cls} invalid Format: {fmt!r}"
|
| 71 |
+
|
| 72 |
+
return fmt, remainder
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class TableBuilder:
|
| 76 |
+
"""
|
| 77 |
+
Helps to populate things derived from BaseTable from maps, tuples, etc.
|
| 78 |
+
|
| 79 |
+
A table of lifecycle callbacks may be provided to add logic beyond what is possible
|
| 80 |
+
based on otData info for the target class. See BuildCallbacks.
|
| 81 |
+
"""
|
| 82 |
+
|
| 83 |
+
def __init__(self, callbackTable=None):
|
| 84 |
+
if callbackTable is None:
|
| 85 |
+
callbackTable = {}
|
| 86 |
+
self._callbackTable = callbackTable
|
| 87 |
+
|
| 88 |
+
def _convert(self, dest, field, converter, value):
|
| 89 |
+
enumClass = getattr(converter, "enumClass", None)
|
| 90 |
+
|
| 91 |
+
if enumClass:
|
| 92 |
+
if isinstance(value, enumClass):
|
| 93 |
+
pass
|
| 94 |
+
elif isinstance(value, str):
|
| 95 |
+
try:
|
| 96 |
+
value = getattr(enumClass, value.upper())
|
| 97 |
+
except AttributeError:
|
| 98 |
+
raise ValueError(f"{value} is not a valid {enumClass}")
|
| 99 |
+
else:
|
| 100 |
+
value = enumClass(value)
|
| 101 |
+
|
| 102 |
+
elif isinstance(converter, IntValue):
|
| 103 |
+
value = otRound(value)
|
| 104 |
+
elif isinstance(converter, FloatValue):
|
| 105 |
+
value = float(value)
|
| 106 |
+
|
| 107 |
+
elif isinstance(converter, Struct):
|
| 108 |
+
if converter.repeat:
|
| 109 |
+
if _isNonStrSequence(value):
|
| 110 |
+
value = [self.build(converter.tableClass, v) for v in value]
|
| 111 |
+
else:
|
| 112 |
+
value = [self.build(converter.tableClass, value)]
|
| 113 |
+
setattr(dest, converter.repeat, len(value))
|
| 114 |
+
else:
|
| 115 |
+
value = self.build(converter.tableClass, value)
|
| 116 |
+
elif callable(converter):
|
| 117 |
+
value = converter(value)
|
| 118 |
+
|
| 119 |
+
setattr(dest, field, value)
|
| 120 |
+
|
| 121 |
+
def build(self, cls, source):
|
| 122 |
+
assert issubclass(cls, BaseTable)
|
| 123 |
+
|
| 124 |
+
if isinstance(source, cls):
|
| 125 |
+
return source
|
| 126 |
+
|
| 127 |
+
callbackKey = (cls,)
|
| 128 |
+
fmt = None
|
| 129 |
+
if issubclass(cls, FormatSwitchingBaseTable):
|
| 130 |
+
fmt, source = _split_format(cls, source)
|
| 131 |
+
callbackKey = (cls, fmt)
|
| 132 |
+
|
| 133 |
+
dest = self._callbackTable.get(
|
| 134 |
+
(BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
|
| 135 |
+
)()
|
| 136 |
+
assert isinstance(dest, cls)
|
| 137 |
+
|
| 138 |
+
convByName = _assignable(cls.convertersByName)
|
| 139 |
+
skippedFields = set()
|
| 140 |
+
|
| 141 |
+
# For format switchers we need to resolve converters based on format
|
| 142 |
+
if issubclass(cls, FormatSwitchingBaseTable):
|
| 143 |
+
dest.Format = fmt
|
| 144 |
+
convByName = _assignable(convByName[dest.Format])
|
| 145 |
+
skippedFields.add("Format")
|
| 146 |
+
|
| 147 |
+
# Convert sequence => mapping so before thunk only has to handle one format
|
| 148 |
+
if _isNonStrSequence(source):
|
| 149 |
+
# Sequence (typically list or tuple) assumed to match fields in declaration order
|
| 150 |
+
assert len(source) <= len(
|
| 151 |
+
convByName
|
| 152 |
+
), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values"
|
| 153 |
+
source = dict(zip(convByName.keys(), source))
|
| 154 |
+
|
| 155 |
+
dest, source = self._callbackTable.get(
|
| 156 |
+
(BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
|
| 157 |
+
)(dest, source)
|
| 158 |
+
|
| 159 |
+
if isinstance(source, collections.abc.Mapping):
|
| 160 |
+
for field, value in source.items():
|
| 161 |
+
if field in skippedFields:
|
| 162 |
+
continue
|
| 163 |
+
converter = convByName.get(field, None)
|
| 164 |
+
if not converter:
|
| 165 |
+
raise ValueError(
|
| 166 |
+
f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}"
|
| 167 |
+
)
|
| 168 |
+
self._convert(dest, field, converter, value)
|
| 169 |
+
else:
|
| 170 |
+
# let's try as a 1-tuple
|
| 171 |
+
dest = self.build(cls, (source,))
|
| 172 |
+
|
| 173 |
+
for field, conv in convByName.items():
|
| 174 |
+
if not hasattr(dest, field) and isinstance(conv, OptionalValue):
|
| 175 |
+
setattr(dest, field, conv.DEFAULT)
|
| 176 |
+
|
| 177 |
+
dest = self._callbackTable.get(
|
| 178 |
+
(BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
|
| 179 |
+
)(dest)
|
| 180 |
+
|
| 181 |
+
return dest
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
class TableUnbuilder:
|
| 185 |
+
def __init__(self, callbackTable=None):
|
| 186 |
+
if callbackTable is None:
|
| 187 |
+
callbackTable = {}
|
| 188 |
+
self._callbackTable = callbackTable
|
| 189 |
+
|
| 190 |
+
def unbuild(self, table):
|
| 191 |
+
assert isinstance(table, BaseTable)
|
| 192 |
+
|
| 193 |
+
source = {}
|
| 194 |
+
|
| 195 |
+
callbackKey = (type(table),)
|
| 196 |
+
if isinstance(table, FormatSwitchingBaseTable):
|
| 197 |
+
source["Format"] = int(table.Format)
|
| 198 |
+
callbackKey += (table.Format,)
|
| 199 |
+
|
| 200 |
+
for converter in table.getConverters():
|
| 201 |
+
if isinstance(converter, ComputedInt):
|
| 202 |
+
continue
|
| 203 |
+
value = getattr(table, converter.name)
|
| 204 |
+
|
| 205 |
+
enumClass = getattr(converter, "enumClass", None)
|
| 206 |
+
if enumClass:
|
| 207 |
+
source[converter.name] = value.name.lower()
|
| 208 |
+
elif isinstance(converter, Struct):
|
| 209 |
+
if converter.repeat:
|
| 210 |
+
source[converter.name] = [self.unbuild(v) for v in value]
|
| 211 |
+
else:
|
| 212 |
+
source[converter.name] = self.unbuild(value)
|
| 213 |
+
elif isinstance(converter, SimpleValue):
|
| 214 |
+
# "simple" values (e.g. int, float, str) need no further un-building
|
| 215 |
+
source[converter.name] = value
|
| 216 |
+
else:
|
| 217 |
+
raise NotImplementedError(
|
| 218 |
+
"Don't know how unbuild {value!r} with {converter!r}"
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
source = self._callbackTable.get(callbackKey, lambda s: s)(source)
|
| 222 |
+
|
| 223 |
+
return source
|
lib/python3.12/site-packages/fontTools/colorLib/unbuilder.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fontTools.ttLib.tables import otTables as ot
|
| 2 |
+
from .table_builder import TableUnbuilder
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def unbuildColrV1(layerList, baseGlyphList):
|
| 6 |
+
layers = []
|
| 7 |
+
if layerList:
|
| 8 |
+
layers = layerList.Paint
|
| 9 |
+
unbuilder = LayerListUnbuilder(layers)
|
| 10 |
+
return {
|
| 11 |
+
rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint)
|
| 12 |
+
for rec in baseGlyphList.BaseGlyphPaintRecord
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _flatten_layers(lst):
|
| 17 |
+
for paint in lst:
|
| 18 |
+
if paint["Format"] == ot.PaintFormat.PaintColrLayers:
|
| 19 |
+
yield from _flatten_layers(paint["Layers"])
|
| 20 |
+
else:
|
| 21 |
+
yield paint
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class LayerListUnbuilder:
|
| 25 |
+
def __init__(self, layers):
|
| 26 |
+
self.layers = layers
|
| 27 |
+
|
| 28 |
+
callbacks = {
|
| 29 |
+
(
|
| 30 |
+
ot.Paint,
|
| 31 |
+
ot.PaintFormat.PaintColrLayers,
|
| 32 |
+
): self._unbuildPaintColrLayers,
|
| 33 |
+
}
|
| 34 |
+
self.tableUnbuilder = TableUnbuilder(callbacks)
|
| 35 |
+
|
| 36 |
+
def unbuildPaint(self, paint):
|
| 37 |
+
assert isinstance(paint, ot.Paint)
|
| 38 |
+
return self.tableUnbuilder.unbuild(paint)
|
| 39 |
+
|
| 40 |
+
def _unbuildPaintColrLayers(self, source):
|
| 41 |
+
assert source["Format"] == ot.PaintFormat.PaintColrLayers
|
| 42 |
+
|
| 43 |
+
layers = list(
|
| 44 |
+
_flatten_layers(
|
| 45 |
+
[
|
| 46 |
+
self.unbuildPaint(childPaint)
|
| 47 |
+
for childPaint in self.layers[
|
| 48 |
+
source["FirstLayerIndex"] : source["FirstLayerIndex"]
|
| 49 |
+
+ source["NumLayers"]
|
| 50 |
+
]
|
| 51 |
+
]
|
| 52 |
+
)
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
if len(layers) == 1:
|
| 56 |
+
return layers[0]
|
| 57 |
+
|
| 58 |
+
return {"Format": source["Format"], "Layers": layers}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
from pprint import pprint
|
| 63 |
+
import sys
|
| 64 |
+
from fontTools.ttLib import TTFont
|
| 65 |
+
|
| 66 |
+
try:
|
| 67 |
+
fontfile = sys.argv[1]
|
| 68 |
+
except IndexError:
|
| 69 |
+
sys.exit("usage: fonttools colorLib.unbuilder FONTFILE")
|
| 70 |
+
|
| 71 |
+
font = TTFont(fontfile)
|
| 72 |
+
colr = font["COLR"]
|
| 73 |
+
if colr.version < 1:
|
| 74 |
+
sys.exit(f"error: No COLR table version=1 found in {fontfile}")
|
| 75 |
+
|
| 76 |
+
colorGlyphs = unbuildColrV1(
|
| 77 |
+
colr.table.LayerList,
|
| 78 |
+
colr.table.BaseGlyphList,
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
pprint(colorGlyphs)
|
lib/python3.12/site-packages/fontTools/designspaceLib/__init__.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
lib/python3.12/site-packages/fontTools/designspaceLib/__main__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from fontTools.designspaceLib import main
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
sys.exit(main())
|
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/__main__.cpython-312.pyc
ADDED
|
Binary file (390 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/split.cpython-312.pyc
ADDED
|
Binary file (17.9 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/statNames.cpython-312.pyc
ADDED
|
Binary file (9.43 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/types.cpython-312.pyc
ADDED
|
Binary file (6.33 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/designspaceLib/split.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Allows building all the variable fonts of a DesignSpace version 5 by
|
| 2 |
+
splitting the document into interpolable sub-space, then into each VF.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import itertools
|
| 8 |
+
import logging
|
| 9 |
+
import math
|
| 10 |
+
from typing import Any, Callable, Dict, Iterator, List, Tuple, cast
|
| 11 |
+
|
| 12 |
+
from fontTools.designspaceLib import (
|
| 13 |
+
AxisDescriptor,
|
| 14 |
+
AxisMappingDescriptor,
|
| 15 |
+
DesignSpaceDocument,
|
| 16 |
+
DiscreteAxisDescriptor,
|
| 17 |
+
InstanceDescriptor,
|
| 18 |
+
RuleDescriptor,
|
| 19 |
+
SimpleLocationDict,
|
| 20 |
+
SourceDescriptor,
|
| 21 |
+
VariableFontDescriptor,
|
| 22 |
+
)
|
| 23 |
+
from fontTools.designspaceLib.statNames import StatNames, getStatNames
|
| 24 |
+
from fontTools.designspaceLib.types import (
|
| 25 |
+
ConditionSet,
|
| 26 |
+
Range,
|
| 27 |
+
Region,
|
| 28 |
+
getVFUserRegion,
|
| 29 |
+
locationInRegion,
|
| 30 |
+
regionInRegion,
|
| 31 |
+
userRegionToDesignRegion,
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
LOGGER = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
MakeInstanceFilenameCallable = Callable[
|
| 37 |
+
[DesignSpaceDocument, InstanceDescriptor, StatNames], str
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def defaultMakeInstanceFilename(
|
| 42 |
+
doc: DesignSpaceDocument, instance: InstanceDescriptor, statNames: StatNames
|
| 43 |
+
) -> str:
|
| 44 |
+
"""Default callable to synthesize an instance filename
|
| 45 |
+
when makeNames=True, for instances that don't specify an instance name
|
| 46 |
+
in the designspace. This part of the name generation can be overriden
|
| 47 |
+
because it's not specified by the STAT table.
|
| 48 |
+
"""
|
| 49 |
+
familyName = instance.familyName or statNames.familyNames.get("en")
|
| 50 |
+
styleName = instance.styleName or statNames.styleNames.get("en")
|
| 51 |
+
return f"{familyName}-{styleName}.ttf"
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def splitInterpolable(
|
| 55 |
+
doc: DesignSpaceDocument,
|
| 56 |
+
makeNames: bool = True,
|
| 57 |
+
expandLocations: bool = True,
|
| 58 |
+
makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
|
| 59 |
+
) -> Iterator[Tuple[SimpleLocationDict, DesignSpaceDocument]]:
|
| 60 |
+
"""Split the given DS5 into several interpolable sub-designspaces.
|
| 61 |
+
There are as many interpolable sub-spaces as there are combinations of
|
| 62 |
+
discrete axis values.
|
| 63 |
+
|
| 64 |
+
E.g. with axes:
|
| 65 |
+
- italic (discrete) Upright or Italic
|
| 66 |
+
- style (discrete) Sans or Serif
|
| 67 |
+
- weight (continuous) 100 to 900
|
| 68 |
+
|
| 69 |
+
There are 4 sub-spaces in which the Weight axis should interpolate:
|
| 70 |
+
(Upright, Sans), (Upright, Serif), (Italic, Sans) and (Italic, Serif).
|
| 71 |
+
|
| 72 |
+
The sub-designspaces still include the full axis definitions and STAT data,
|
| 73 |
+
but the rules, sources, variable fonts, instances are trimmed down to only
|
| 74 |
+
keep what falls within the interpolable sub-space.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
- ``makeNames``: Whether to compute the instance family and style
|
| 78 |
+
names using the STAT data.
|
| 79 |
+
- ``expandLocations``: Whether to turn all locations into "full"
|
| 80 |
+
locations, including implicit default axis values where missing.
|
| 81 |
+
- ``makeInstanceFilename``: Callable to synthesize an instance filename
|
| 82 |
+
when makeNames=True, for instances that don't specify an instance name
|
| 83 |
+
in the designspace. This part of the name generation can be overridden
|
| 84 |
+
because it's not specified by the STAT table.
|
| 85 |
+
|
| 86 |
+
.. versionadded:: 5.0
|
| 87 |
+
"""
|
| 88 |
+
discreteAxes = []
|
| 89 |
+
interpolableUserRegion: Region = {}
|
| 90 |
+
for axis in doc.axes:
|
| 91 |
+
if hasattr(axis, "values"):
|
| 92 |
+
# Mypy doesn't support narrowing union types via hasattr()
|
| 93 |
+
# TODO(Python 3.10): use TypeGuard
|
| 94 |
+
# https://mypy.readthedocs.io/en/stable/type_narrowing.html
|
| 95 |
+
axis = cast(DiscreteAxisDescriptor, axis)
|
| 96 |
+
discreteAxes.append(axis)
|
| 97 |
+
else:
|
| 98 |
+
axis = cast(AxisDescriptor, axis)
|
| 99 |
+
interpolableUserRegion[axis.name] = Range(
|
| 100 |
+
axis.minimum,
|
| 101 |
+
axis.maximum,
|
| 102 |
+
axis.default,
|
| 103 |
+
)
|
| 104 |
+
valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
|
| 105 |
+
for values in valueCombinations:
|
| 106 |
+
discreteUserLocation = {
|
| 107 |
+
discreteAxis.name: value
|
| 108 |
+
for discreteAxis, value in zip(discreteAxes, values)
|
| 109 |
+
}
|
| 110 |
+
subDoc = _extractSubSpace(
|
| 111 |
+
doc,
|
| 112 |
+
{**interpolableUserRegion, **discreteUserLocation},
|
| 113 |
+
keepVFs=True,
|
| 114 |
+
makeNames=makeNames,
|
| 115 |
+
expandLocations=expandLocations,
|
| 116 |
+
makeInstanceFilename=makeInstanceFilename,
|
| 117 |
+
)
|
| 118 |
+
yield discreteUserLocation, subDoc
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def splitVariableFonts(
|
| 122 |
+
doc: DesignSpaceDocument,
|
| 123 |
+
makeNames: bool = False,
|
| 124 |
+
expandLocations: bool = False,
|
| 125 |
+
makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
|
| 126 |
+
) -> Iterator[Tuple[str, DesignSpaceDocument]]:
|
| 127 |
+
"""Convert each variable font listed in this document into a standalone
|
| 128 |
+
designspace. This can be used to compile all the variable fonts from a
|
| 129 |
+
format 5 designspace using tools that can only deal with 1 VF at a time.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
- ``makeNames``: Whether to compute the instance family and style
|
| 133 |
+
names using the STAT data.
|
| 134 |
+
- ``expandLocations``: Whether to turn all locations into "full"
|
| 135 |
+
locations, including implicit default axis values where missing.
|
| 136 |
+
- ``makeInstanceFilename``: Callable to synthesize an instance filename
|
| 137 |
+
when makeNames=True, for instances that don't specify an instance name
|
| 138 |
+
in the designspace. This part of the name generation can be overridden
|
| 139 |
+
because it's not specified by the STAT table.
|
| 140 |
+
|
| 141 |
+
.. versionadded:: 5.0
|
| 142 |
+
"""
|
| 143 |
+
# Make one DesignspaceDoc v5 for each variable font
|
| 144 |
+
for vf in doc.getVariableFonts():
|
| 145 |
+
vfUserRegion = getVFUserRegion(doc, vf)
|
| 146 |
+
vfDoc = _extractSubSpace(
|
| 147 |
+
doc,
|
| 148 |
+
vfUserRegion,
|
| 149 |
+
keepVFs=False,
|
| 150 |
+
makeNames=makeNames,
|
| 151 |
+
expandLocations=expandLocations,
|
| 152 |
+
makeInstanceFilename=makeInstanceFilename,
|
| 153 |
+
)
|
| 154 |
+
vfDoc.lib = {**vfDoc.lib, **vf.lib}
|
| 155 |
+
yield vf.name, vfDoc
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def convert5to4(
|
| 159 |
+
doc: DesignSpaceDocument,
|
| 160 |
+
) -> Dict[str, DesignSpaceDocument]:
|
| 161 |
+
"""Convert each variable font listed in this document into a standalone
|
| 162 |
+
format 4 designspace. This can be used to compile all the variable fonts
|
| 163 |
+
from a format 5 designspace using tools that only know about format 4.
|
| 164 |
+
|
| 165 |
+
.. versionadded:: 5.0
|
| 166 |
+
"""
|
| 167 |
+
vfs = {}
|
| 168 |
+
for _location, subDoc in splitInterpolable(doc):
|
| 169 |
+
for vfName, vfDoc in splitVariableFonts(subDoc):
|
| 170 |
+
vfDoc.formatVersion = "4.1"
|
| 171 |
+
vfs[vfName] = vfDoc
|
| 172 |
+
return vfs
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _extractSubSpace(
|
| 176 |
+
doc: DesignSpaceDocument,
|
| 177 |
+
userRegion: Region,
|
| 178 |
+
*,
|
| 179 |
+
keepVFs: bool,
|
| 180 |
+
makeNames: bool,
|
| 181 |
+
expandLocations: bool,
|
| 182 |
+
makeInstanceFilename: MakeInstanceFilenameCallable,
|
| 183 |
+
) -> DesignSpaceDocument:
|
| 184 |
+
subDoc = DesignSpaceDocument()
|
| 185 |
+
# Don't include STAT info
|
| 186 |
+
# FIXME: (Jany) let's think about it. Not include = OK because the point of
|
| 187 |
+
# the splitting is to build VFs and we'll use the STAT data of the full
|
| 188 |
+
# document to generate the STAT of the VFs, so "no need" to have STAT data
|
| 189 |
+
# in sub-docs. Counterpoint: what if someone wants to split this DS for
|
| 190 |
+
# other purposes? Maybe for that it would be useful to also subset the STAT
|
| 191 |
+
# data?
|
| 192 |
+
# subDoc.elidedFallbackName = doc.elidedFallbackName
|
| 193 |
+
|
| 194 |
+
def maybeExpandDesignLocation(object):
|
| 195 |
+
if expandLocations:
|
| 196 |
+
return object.getFullDesignLocation(doc)
|
| 197 |
+
else:
|
| 198 |
+
return object.designLocation
|
| 199 |
+
|
| 200 |
+
for axis in doc.axes:
|
| 201 |
+
range = userRegion[axis.name]
|
| 202 |
+
if isinstance(range, Range) and hasattr(axis, "minimum"):
|
| 203 |
+
# Mypy doesn't support narrowing union types via hasattr()
|
| 204 |
+
# TODO(Python 3.10): use TypeGuard
|
| 205 |
+
# https://mypy.readthedocs.io/en/stable/type_narrowing.html
|
| 206 |
+
axis = cast(AxisDescriptor, axis)
|
| 207 |
+
subDoc.addAxis(
|
| 208 |
+
AxisDescriptor(
|
| 209 |
+
# Same info
|
| 210 |
+
tag=axis.tag,
|
| 211 |
+
name=axis.name,
|
| 212 |
+
labelNames=axis.labelNames,
|
| 213 |
+
hidden=axis.hidden,
|
| 214 |
+
# Subset range
|
| 215 |
+
minimum=max(range.minimum, axis.minimum),
|
| 216 |
+
default=range.default or axis.default,
|
| 217 |
+
maximum=min(range.maximum, axis.maximum),
|
| 218 |
+
map=[
|
| 219 |
+
(user, design)
|
| 220 |
+
for user, design in axis.map
|
| 221 |
+
if range.minimum <= user <= range.maximum
|
| 222 |
+
],
|
| 223 |
+
# Don't include STAT info
|
| 224 |
+
axisOrdering=None,
|
| 225 |
+
axisLabels=None,
|
| 226 |
+
)
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
subDoc.axisMappings = mappings = []
|
| 230 |
+
subDocAxes = {axis.name for axis in subDoc.axes}
|
| 231 |
+
for mapping in doc.axisMappings:
|
| 232 |
+
if not all(axis in subDocAxes for axis in mapping.inputLocation.keys()):
|
| 233 |
+
continue
|
| 234 |
+
if not all(axis in subDocAxes for axis in mapping.outputLocation.keys()):
|
| 235 |
+
LOGGER.error(
|
| 236 |
+
"In axis mapping from input %s, some output axes are not in the variable-font: %s",
|
| 237 |
+
mapping.inputLocation,
|
| 238 |
+
mapping.outputLocation,
|
| 239 |
+
)
|
| 240 |
+
continue
|
| 241 |
+
|
| 242 |
+
mappingAxes = set()
|
| 243 |
+
mappingAxes.update(mapping.inputLocation.keys())
|
| 244 |
+
mappingAxes.update(mapping.outputLocation.keys())
|
| 245 |
+
for axis in doc.axes:
|
| 246 |
+
if axis.name not in mappingAxes:
|
| 247 |
+
continue
|
| 248 |
+
range = userRegion[axis.name]
|
| 249 |
+
if (
|
| 250 |
+
range.minimum != axis.minimum
|
| 251 |
+
or (range.default is not None and range.default != axis.default)
|
| 252 |
+
or range.maximum != axis.maximum
|
| 253 |
+
):
|
| 254 |
+
LOGGER.error(
|
| 255 |
+
"Limiting axis ranges used in <mapping> elements not supported: %s",
|
| 256 |
+
axis.name,
|
| 257 |
+
)
|
| 258 |
+
continue
|
| 259 |
+
|
| 260 |
+
mappings.append(
|
| 261 |
+
AxisMappingDescriptor(
|
| 262 |
+
inputLocation=mapping.inputLocation,
|
| 263 |
+
outputLocation=mapping.outputLocation,
|
| 264 |
+
)
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# Don't include STAT info
|
| 268 |
+
# subDoc.locationLabels = doc.locationLabels
|
| 269 |
+
|
| 270 |
+
# Rules: subset them based on conditions
|
| 271 |
+
designRegion = userRegionToDesignRegion(doc, userRegion)
|
| 272 |
+
subDoc.rules = _subsetRulesBasedOnConditions(doc.rules, designRegion)
|
| 273 |
+
subDoc.rulesProcessingLast = doc.rulesProcessingLast
|
| 274 |
+
|
| 275 |
+
# Sources: keep only the ones that fall within the kept axis ranges
|
| 276 |
+
for source in doc.sources:
|
| 277 |
+
if not locationInRegion(doc.map_backward(source.designLocation), userRegion):
|
| 278 |
+
continue
|
| 279 |
+
|
| 280 |
+
subDoc.addSource(
|
| 281 |
+
SourceDescriptor(
|
| 282 |
+
filename=source.filename,
|
| 283 |
+
path=source.path,
|
| 284 |
+
font=source.font,
|
| 285 |
+
name=source.name,
|
| 286 |
+
designLocation=_filterLocation(
|
| 287 |
+
userRegion, maybeExpandDesignLocation(source)
|
| 288 |
+
),
|
| 289 |
+
layerName=source.layerName,
|
| 290 |
+
familyName=source.familyName,
|
| 291 |
+
styleName=source.styleName,
|
| 292 |
+
muteKerning=source.muteKerning,
|
| 293 |
+
muteInfo=source.muteInfo,
|
| 294 |
+
mutedGlyphNames=source.mutedGlyphNames,
|
| 295 |
+
)
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# Copy family name translations from the old default source to the new default
|
| 299 |
+
vfDefault = subDoc.findDefault()
|
| 300 |
+
oldDefault = doc.findDefault()
|
| 301 |
+
if vfDefault is not None and oldDefault is not None:
|
| 302 |
+
vfDefault.localisedFamilyName = oldDefault.localisedFamilyName
|
| 303 |
+
|
| 304 |
+
# Variable fonts: keep only the ones that fall within the kept axis ranges
|
| 305 |
+
if keepVFs:
|
| 306 |
+
# Note: call getVariableFont() to make the implicit VFs explicit
|
| 307 |
+
for vf in doc.getVariableFonts():
|
| 308 |
+
vfUserRegion = getVFUserRegion(doc, vf)
|
| 309 |
+
if regionInRegion(vfUserRegion, userRegion):
|
| 310 |
+
subDoc.addVariableFont(
|
| 311 |
+
VariableFontDescriptor(
|
| 312 |
+
name=vf.name,
|
| 313 |
+
filename=vf.filename,
|
| 314 |
+
axisSubsets=[
|
| 315 |
+
axisSubset
|
| 316 |
+
for axisSubset in vf.axisSubsets
|
| 317 |
+
if isinstance(userRegion[axisSubset.name], Range)
|
| 318 |
+
],
|
| 319 |
+
lib=vf.lib,
|
| 320 |
+
)
|
| 321 |
+
)
|
| 322 |
+
|
| 323 |
+
# Instances: same as Sources + compute missing names
|
| 324 |
+
for instance in doc.instances:
|
| 325 |
+
if not locationInRegion(instance.getFullUserLocation(doc), userRegion):
|
| 326 |
+
continue
|
| 327 |
+
|
| 328 |
+
if makeNames:
|
| 329 |
+
statNames = getStatNames(doc, instance.getFullUserLocation(doc))
|
| 330 |
+
familyName = instance.familyName or statNames.familyNames.get("en")
|
| 331 |
+
styleName = instance.styleName or statNames.styleNames.get("en")
|
| 332 |
+
subDoc.addInstance(
|
| 333 |
+
InstanceDescriptor(
|
| 334 |
+
filename=instance.filename
|
| 335 |
+
or makeInstanceFilename(doc, instance, statNames),
|
| 336 |
+
path=instance.path,
|
| 337 |
+
font=instance.font,
|
| 338 |
+
name=instance.name or f"{familyName} {styleName}",
|
| 339 |
+
userLocation={} if expandLocations else instance.userLocation,
|
| 340 |
+
designLocation=_filterLocation(
|
| 341 |
+
userRegion, maybeExpandDesignLocation(instance)
|
| 342 |
+
),
|
| 343 |
+
familyName=familyName,
|
| 344 |
+
styleName=styleName,
|
| 345 |
+
postScriptFontName=instance.postScriptFontName
|
| 346 |
+
or statNames.postScriptFontName,
|
| 347 |
+
styleMapFamilyName=instance.styleMapFamilyName
|
| 348 |
+
or statNames.styleMapFamilyNames.get("en"),
|
| 349 |
+
styleMapStyleName=instance.styleMapStyleName
|
| 350 |
+
or statNames.styleMapStyleName,
|
| 351 |
+
localisedFamilyName=instance.localisedFamilyName
|
| 352 |
+
or statNames.familyNames,
|
| 353 |
+
localisedStyleName=instance.localisedStyleName
|
| 354 |
+
or statNames.styleNames,
|
| 355 |
+
localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName
|
| 356 |
+
or statNames.styleMapFamilyNames,
|
| 357 |
+
localisedStyleMapStyleName=instance.localisedStyleMapStyleName
|
| 358 |
+
or {},
|
| 359 |
+
lib=instance.lib,
|
| 360 |
+
)
|
| 361 |
+
)
|
| 362 |
+
else:
|
| 363 |
+
subDoc.addInstance(
|
| 364 |
+
InstanceDescriptor(
|
| 365 |
+
filename=instance.filename,
|
| 366 |
+
path=instance.path,
|
| 367 |
+
font=instance.font,
|
| 368 |
+
name=instance.name,
|
| 369 |
+
userLocation={} if expandLocations else instance.userLocation,
|
| 370 |
+
designLocation=_filterLocation(
|
| 371 |
+
userRegion, maybeExpandDesignLocation(instance)
|
| 372 |
+
),
|
| 373 |
+
familyName=instance.familyName,
|
| 374 |
+
styleName=instance.styleName,
|
| 375 |
+
postScriptFontName=instance.postScriptFontName,
|
| 376 |
+
styleMapFamilyName=instance.styleMapFamilyName,
|
| 377 |
+
styleMapStyleName=instance.styleMapStyleName,
|
| 378 |
+
localisedFamilyName=instance.localisedFamilyName,
|
| 379 |
+
localisedStyleName=instance.localisedStyleName,
|
| 380 |
+
localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName,
|
| 381 |
+
localisedStyleMapStyleName=instance.localisedStyleMapStyleName,
|
| 382 |
+
lib=instance.lib,
|
| 383 |
+
)
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
subDoc.lib = doc.lib
|
| 387 |
+
|
| 388 |
+
return subDoc
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def _conditionSetFrom(conditionSet: List[Dict[str, Any]]) -> ConditionSet:
|
| 392 |
+
c: Dict[str, Range] = {}
|
| 393 |
+
for condition in conditionSet:
|
| 394 |
+
minimum, maximum = condition.get("minimum"), condition.get("maximum")
|
| 395 |
+
c[condition["name"]] = Range(
|
| 396 |
+
minimum if minimum is not None else -math.inf,
|
| 397 |
+
maximum if maximum is not None else math.inf,
|
| 398 |
+
)
|
| 399 |
+
return c
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def _subsetRulesBasedOnConditions(
|
| 403 |
+
rules: List[RuleDescriptor], designRegion: Region
|
| 404 |
+
) -> List[RuleDescriptor]:
|
| 405 |
+
# What rules to keep:
|
| 406 |
+
# - Keep the rule if any conditionset is relevant.
|
| 407 |
+
# - A conditionset is relevant if all conditions are relevant or it is empty.
|
| 408 |
+
# - A condition is relevant if
|
| 409 |
+
# - axis is point (C-AP),
|
| 410 |
+
# - and point in condition's range (C-AP-in)
|
| 411 |
+
# (in this case remove the condition because it's always true)
|
| 412 |
+
# - else (C-AP-out) whole conditionset can be discarded (condition false
|
| 413 |
+
# => conditionset false)
|
| 414 |
+
# - axis is range (C-AR),
|
| 415 |
+
# - (C-AR-all) and axis range fully contained in condition range: we can
|
| 416 |
+
# scrap the condition because it's always true
|
| 417 |
+
# - (C-AR-inter) and intersection(axis range, condition range) not empty:
|
| 418 |
+
# keep the condition with the smaller range (= intersection)
|
| 419 |
+
# - (C-AR-none) else, whole conditionset can be discarded
|
| 420 |
+
newRules: List[RuleDescriptor] = []
|
| 421 |
+
for rule in rules:
|
| 422 |
+
newRule: RuleDescriptor = RuleDescriptor(
|
| 423 |
+
name=rule.name, conditionSets=[], subs=rule.subs
|
| 424 |
+
)
|
| 425 |
+
for conditionset in rule.conditionSets:
|
| 426 |
+
cs = _conditionSetFrom(conditionset)
|
| 427 |
+
newConditionset: List[Dict[str, Any]] = []
|
| 428 |
+
discardConditionset = False
|
| 429 |
+
for selectionName, selectionValue in designRegion.items():
|
| 430 |
+
# TODO: Ensure that all(key in conditionset for key in region.keys())?
|
| 431 |
+
if selectionName not in cs:
|
| 432 |
+
# raise Exception("Selection has different axes than the rules")
|
| 433 |
+
continue
|
| 434 |
+
if isinstance(selectionValue, (float, int)): # is point
|
| 435 |
+
# Case C-AP-in
|
| 436 |
+
if selectionValue in cs[selectionName]:
|
| 437 |
+
pass # always matches, conditionset can stay empty for this one.
|
| 438 |
+
# Case C-AP-out
|
| 439 |
+
else:
|
| 440 |
+
discardConditionset = True
|
| 441 |
+
else: # is range
|
| 442 |
+
# Case C-AR-all
|
| 443 |
+
if selectionValue in cs[selectionName]:
|
| 444 |
+
pass # always matches, conditionset can stay empty for this one.
|
| 445 |
+
else:
|
| 446 |
+
intersection = cs[selectionName].intersection(selectionValue)
|
| 447 |
+
# Case C-AR-inter
|
| 448 |
+
if intersection is not None:
|
| 449 |
+
newConditionset.append(
|
| 450 |
+
{
|
| 451 |
+
"name": selectionName,
|
| 452 |
+
"minimum": intersection.minimum,
|
| 453 |
+
"maximum": intersection.maximum,
|
| 454 |
+
}
|
| 455 |
+
)
|
| 456 |
+
# Case C-AR-none
|
| 457 |
+
else:
|
| 458 |
+
discardConditionset = True
|
| 459 |
+
if not discardConditionset:
|
| 460 |
+
newRule.conditionSets.append(newConditionset)
|
| 461 |
+
if newRule.conditionSets:
|
| 462 |
+
newRules.append(newRule)
|
| 463 |
+
|
| 464 |
+
return newRules
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
def _filterLocation(
|
| 468 |
+
userRegion: Region,
|
| 469 |
+
location: Dict[str, float],
|
| 470 |
+
) -> Dict[str, float]:
|
| 471 |
+
return {
|
| 472 |
+
name: value
|
| 473 |
+
for name, value in location.items()
|
| 474 |
+
if name in userRegion and isinstance(userRegion[name], Range)
|
| 475 |
+
}
|
lib/python3.12/site-packages/fontTools/designspaceLib/statNames.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compute name information for a given location in user-space coordinates
|
| 2 |
+
using STAT data. This can be used to fill-in automatically the names of an
|
| 3 |
+
instance:
|
| 4 |
+
|
| 5 |
+
.. code:: python
|
| 6 |
+
|
| 7 |
+
instance = doc.instances[0]
|
| 8 |
+
names = getStatNames(doc, instance.getFullUserLocation(doc))
|
| 9 |
+
print(names.styleNames)
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
from dataclasses import dataclass
|
| 15 |
+
from typing import Dict, Literal, Optional, Tuple, Union
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
from fontTools.designspaceLib import (
|
| 19 |
+
AxisDescriptor,
|
| 20 |
+
AxisLabelDescriptor,
|
| 21 |
+
DesignSpaceDocument,
|
| 22 |
+
DiscreteAxisDescriptor,
|
| 23 |
+
SimpleLocationDict,
|
| 24 |
+
SourceDescriptor,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
LOGGER = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
RibbiStyleName = Union[
|
| 30 |
+
Literal["regular"],
|
| 31 |
+
Literal["bold"],
|
| 32 |
+
Literal["italic"],
|
| 33 |
+
Literal["bold italic"],
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
BOLD_ITALIC_TO_RIBBI_STYLE = {
|
| 37 |
+
(False, False): "regular",
|
| 38 |
+
(False, True): "italic",
|
| 39 |
+
(True, False): "bold",
|
| 40 |
+
(True, True): "bold italic",
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class StatNames:
|
| 46 |
+
"""Name data generated from the STAT table information."""
|
| 47 |
+
|
| 48 |
+
familyNames: Dict[str, str]
|
| 49 |
+
styleNames: Dict[str, str]
|
| 50 |
+
postScriptFontName: Optional[str]
|
| 51 |
+
styleMapFamilyNames: Dict[str, str]
|
| 52 |
+
styleMapStyleName: Optional[RibbiStyleName]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def getStatNames(
|
| 56 |
+
doc: DesignSpaceDocument, userLocation: SimpleLocationDict
|
| 57 |
+
) -> StatNames:
|
| 58 |
+
"""Compute the family, style, PostScript names of the given ``userLocation``
|
| 59 |
+
using the document's STAT information.
|
| 60 |
+
|
| 61 |
+
Also computes localizations.
|
| 62 |
+
|
| 63 |
+
If not enough STAT data is available for a given name, either its dict of
|
| 64 |
+
localized names will be empty (family and style names), or the name will be
|
| 65 |
+
None (PostScript name).
|
| 66 |
+
|
| 67 |
+
Note: this method does not consider info attached to the instance, like
|
| 68 |
+
family name. The user needs to override all names on an instance that STAT
|
| 69 |
+
information would compute differently than desired.
|
| 70 |
+
|
| 71 |
+
.. versionadded:: 5.0
|
| 72 |
+
"""
|
| 73 |
+
familyNames: Dict[str, str] = {}
|
| 74 |
+
defaultSource: Optional[SourceDescriptor] = doc.findDefault()
|
| 75 |
+
if defaultSource is None:
|
| 76 |
+
LOGGER.warning("Cannot determine default source to look up family name.")
|
| 77 |
+
elif defaultSource.familyName is None:
|
| 78 |
+
LOGGER.warning(
|
| 79 |
+
"Cannot look up family name, assign the 'familyname' attribute to the default source."
|
| 80 |
+
)
|
| 81 |
+
else:
|
| 82 |
+
familyNames = {
|
| 83 |
+
"en": defaultSource.familyName,
|
| 84 |
+
**defaultSource.localisedFamilyName,
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
styleNames: Dict[str, str] = {}
|
| 88 |
+
# If a free-standing label matches the location, use it for name generation.
|
| 89 |
+
label = doc.labelForUserLocation(userLocation)
|
| 90 |
+
if label is not None:
|
| 91 |
+
styleNames = {"en": label.name, **label.labelNames}
|
| 92 |
+
# Otherwise, scour the axis labels for matches.
|
| 93 |
+
else:
|
| 94 |
+
# Gather all languages in which at least one translation is provided
|
| 95 |
+
# Then build names for all these languages, but fallback to English
|
| 96 |
+
# whenever a translation is missing.
|
| 97 |
+
labels = _getAxisLabelsForUserLocation(doc.axes, userLocation)
|
| 98 |
+
if labels:
|
| 99 |
+
languages = set(
|
| 100 |
+
language for label in labels for language in label.labelNames
|
| 101 |
+
)
|
| 102 |
+
languages.add("en")
|
| 103 |
+
for language in languages:
|
| 104 |
+
styleName = " ".join(
|
| 105 |
+
label.labelNames.get(language, label.defaultName)
|
| 106 |
+
for label in labels
|
| 107 |
+
if not label.elidable
|
| 108 |
+
)
|
| 109 |
+
if not styleName and doc.elidedFallbackName is not None:
|
| 110 |
+
styleName = doc.elidedFallbackName
|
| 111 |
+
styleNames[language] = styleName
|
| 112 |
+
|
| 113 |
+
if "en" not in familyNames or "en" not in styleNames:
|
| 114 |
+
# Not enough information to compute PS names of styleMap names
|
| 115 |
+
return StatNames(
|
| 116 |
+
familyNames=familyNames,
|
| 117 |
+
styleNames=styleNames,
|
| 118 |
+
postScriptFontName=None,
|
| 119 |
+
styleMapFamilyNames={},
|
| 120 |
+
styleMapStyleName=None,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")
|
| 124 |
+
|
| 125 |
+
styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation)
|
| 126 |
+
|
| 127 |
+
styleNamesForStyleMap = styleNames
|
| 128 |
+
if regularUserLocation != userLocation:
|
| 129 |
+
regularStatNames = getStatNames(doc, regularUserLocation)
|
| 130 |
+
styleNamesForStyleMap = regularStatNames.styleNames
|
| 131 |
+
|
| 132 |
+
styleMapFamilyNames = {}
|
| 133 |
+
for language in set(familyNames).union(styleNames.keys()):
|
| 134 |
+
familyName = familyNames.get(language, familyNames["en"])
|
| 135 |
+
styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"])
|
| 136 |
+
styleMapFamilyNames[language] = (familyName + " " + styleName).strip()
|
| 137 |
+
|
| 138 |
+
return StatNames(
|
| 139 |
+
familyNames=familyNames,
|
| 140 |
+
styleNames=styleNames,
|
| 141 |
+
postScriptFontName=postScriptFontName,
|
| 142 |
+
styleMapFamilyNames=styleMapFamilyNames,
|
| 143 |
+
styleMapStyleName=styleMapStyleName,
|
| 144 |
+
)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _getSortedAxisLabels(
|
| 148 |
+
axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
|
| 149 |
+
) -> Dict[str, list[AxisLabelDescriptor]]:
|
| 150 |
+
"""Returns axis labels sorted by their ordering, with unordered ones appended as
|
| 151 |
+
they are listed."""
|
| 152 |
+
|
| 153 |
+
# First, get the axis labels with explicit ordering...
|
| 154 |
+
sortedAxes = sorted(
|
| 155 |
+
(axis for axis in axes if axis.axisOrdering is not None),
|
| 156 |
+
key=lambda a: a.axisOrdering,
|
| 157 |
+
)
|
| 158 |
+
sortedLabels: Dict[str, list[AxisLabelDescriptor]] = {
|
| 159 |
+
axis.name: axis.axisLabels for axis in sortedAxes
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
# ... then append the others in the order they appear.
|
| 163 |
+
# NOTE: This relies on Python 3.7+ dict's preserved insertion order.
|
| 164 |
+
for axis in axes:
|
| 165 |
+
if axis.axisOrdering is None:
|
| 166 |
+
sortedLabels[axis.name] = axis.axisLabels
|
| 167 |
+
|
| 168 |
+
return sortedLabels
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _getAxisLabelsForUserLocation(
|
| 172 |
+
axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
|
| 173 |
+
userLocation: SimpleLocationDict,
|
| 174 |
+
) -> list[AxisLabelDescriptor]:
|
| 175 |
+
labels: list[AxisLabelDescriptor] = []
|
| 176 |
+
|
| 177 |
+
allAxisLabels = _getSortedAxisLabels(axes)
|
| 178 |
+
if allAxisLabels.keys() != userLocation.keys():
|
| 179 |
+
LOGGER.warning(
|
| 180 |
+
f"Mismatch between user location '{userLocation.keys()}' and available "
|
| 181 |
+
f"labels for '{allAxisLabels.keys()}'."
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
for axisName, axisLabels in allAxisLabels.items():
|
| 185 |
+
userValue = userLocation[axisName]
|
| 186 |
+
label: Optional[AxisLabelDescriptor] = next(
|
| 187 |
+
(
|
| 188 |
+
l
|
| 189 |
+
for l in axisLabels
|
| 190 |
+
if l.userValue == userValue
|
| 191 |
+
or (
|
| 192 |
+
l.userMinimum is not None
|
| 193 |
+
and l.userMaximum is not None
|
| 194 |
+
and l.userMinimum <= userValue <= l.userMaximum
|
| 195 |
+
)
|
| 196 |
+
),
|
| 197 |
+
None,
|
| 198 |
+
)
|
| 199 |
+
if label is None:
|
| 200 |
+
LOGGER.debug(
|
| 201 |
+
f"Document needs a label for axis '{axisName}', user value '{userValue}'."
|
| 202 |
+
)
|
| 203 |
+
else:
|
| 204 |
+
labels.append(label)
|
| 205 |
+
|
| 206 |
+
return labels
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _getRibbiStyle(
|
| 210 |
+
self: DesignSpaceDocument, userLocation: SimpleLocationDict
|
| 211 |
+
) -> Tuple[RibbiStyleName, SimpleLocationDict]:
|
| 212 |
+
"""Compute the RIBBI style name of the given user location,
|
| 213 |
+
return the location of the matching Regular in the RIBBI group.
|
| 214 |
+
|
| 215 |
+
.. versionadded:: 5.0
|
| 216 |
+
"""
|
| 217 |
+
regularUserLocation = {}
|
| 218 |
+
axes_by_tag = {axis.tag: axis for axis in self.axes}
|
| 219 |
+
|
| 220 |
+
bold: bool = False
|
| 221 |
+
italic: bool = False
|
| 222 |
+
|
| 223 |
+
axis = axes_by_tag.get("wght")
|
| 224 |
+
if axis is not None:
|
| 225 |
+
for regular_label in axis.axisLabels:
|
| 226 |
+
if (
|
| 227 |
+
regular_label.linkedUserValue == userLocation[axis.name]
|
| 228 |
+
# In the "recursive" case where both the Regular has
|
| 229 |
+
# linkedUserValue pointing the Bold, and the Bold has
|
| 230 |
+
# linkedUserValue pointing to the Regular, only consider the
|
| 231 |
+
# first case: Regular (e.g. 400) has linkedUserValue pointing to
|
| 232 |
+
# Bold (e.g. 700, higher than Regular)
|
| 233 |
+
and regular_label.userValue < regular_label.linkedUserValue
|
| 234 |
+
):
|
| 235 |
+
regularUserLocation[axis.name] = regular_label.userValue
|
| 236 |
+
bold = True
|
| 237 |
+
break
|
| 238 |
+
|
| 239 |
+
axis = axes_by_tag.get("ital") or axes_by_tag.get("slnt")
|
| 240 |
+
if axis is not None:
|
| 241 |
+
for upright_label in axis.axisLabels:
|
| 242 |
+
if (
|
| 243 |
+
upright_label.linkedUserValue == userLocation[axis.name]
|
| 244 |
+
# In the "recursive" case where both the Upright has
|
| 245 |
+
# linkedUserValue pointing the Italic, and the Italic has
|
| 246 |
+
# linkedUserValue pointing to the Upright, only consider the
|
| 247 |
+
# first case: Upright (e.g. ital=0, slant=0) has
|
| 248 |
+
# linkedUserValue pointing to Italic (e.g ital=1, slant=-12 or
|
| 249 |
+
# slant=12 for backwards italics, in any case higher than
|
| 250 |
+
# Upright in absolute value, hence the abs() below.
|
| 251 |
+
and abs(upright_label.userValue) < abs(upright_label.linkedUserValue)
|
| 252 |
+
):
|
| 253 |
+
regularUserLocation[axis.name] = upright_label.userValue
|
| 254 |
+
italic = True
|
| 255 |
+
break
|
| 256 |
+
|
| 257 |
+
return BOLD_ITALIC_TO_RIBBI_STYLE[bold, italic], {
|
| 258 |
+
**userLocation,
|
| 259 |
+
**regularUserLocation,
|
| 260 |
+
}
|
lib/python3.12/site-packages/fontTools/designspaceLib/types.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import Dict, List, Optional, Union, cast
|
| 5 |
+
|
| 6 |
+
from fontTools.designspaceLib import (
|
| 7 |
+
AxisDescriptor,
|
| 8 |
+
DesignSpaceDocument,
|
| 9 |
+
DesignSpaceDocumentError,
|
| 10 |
+
RangeAxisSubsetDescriptor,
|
| 11 |
+
SimpleLocationDict,
|
| 12 |
+
ValueAxisSubsetDescriptor,
|
| 13 |
+
VariableFontDescriptor,
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def clamp(value, minimum, maximum):
|
| 18 |
+
return min(max(value, minimum), maximum)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class Range:
|
| 23 |
+
minimum: float
|
| 24 |
+
"""Inclusive minimum of the range."""
|
| 25 |
+
maximum: float
|
| 26 |
+
"""Inclusive maximum of the range."""
|
| 27 |
+
default: float = 0
|
| 28 |
+
"""Default value"""
|
| 29 |
+
|
| 30 |
+
def __post_init__(self):
|
| 31 |
+
self.minimum, self.maximum = sorted((self.minimum, self.maximum))
|
| 32 |
+
self.default = clamp(self.default, self.minimum, self.maximum)
|
| 33 |
+
|
| 34 |
+
def __contains__(self, value: Union[float, Range]) -> bool:
|
| 35 |
+
if isinstance(value, Range):
|
| 36 |
+
return self.minimum <= value.minimum and value.maximum <= self.maximum
|
| 37 |
+
return self.minimum <= value <= self.maximum
|
| 38 |
+
|
| 39 |
+
def intersection(self, other: Range) -> Optional[Range]:
|
| 40 |
+
if self.maximum < other.minimum or self.minimum > other.maximum:
|
| 41 |
+
return None
|
| 42 |
+
else:
|
| 43 |
+
return Range(
|
| 44 |
+
max(self.minimum, other.minimum),
|
| 45 |
+
min(self.maximum, other.maximum),
|
| 46 |
+
self.default, # We don't care about the default in this use-case
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# A region selection is either a range or a single value, as a Designspace v5
|
| 51 |
+
# axis-subset element only allows a single discrete value or a range for a
|
| 52 |
+
# variable-font element.
|
| 53 |
+
Region = Dict[str, Union[Range, float]]
|
| 54 |
+
|
| 55 |
+
# A conditionset is a set of named ranges.
|
| 56 |
+
ConditionSet = Dict[str, Range]
|
| 57 |
+
|
| 58 |
+
# A rule is a list of conditionsets where any has to be relevant for the whole rule to be relevant.
|
| 59 |
+
Rule = List[ConditionSet]
|
| 60 |
+
Rules = Dict[str, Rule]
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def locationInRegion(location: SimpleLocationDict, region: Region) -> bool:
|
| 64 |
+
for name, value in location.items():
|
| 65 |
+
if name not in region:
|
| 66 |
+
return False
|
| 67 |
+
regionValue = region[name]
|
| 68 |
+
if isinstance(regionValue, (float, int)):
|
| 69 |
+
if value != regionValue:
|
| 70 |
+
return False
|
| 71 |
+
else:
|
| 72 |
+
if value not in regionValue:
|
| 73 |
+
return False
|
| 74 |
+
return True
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def regionInRegion(region: Region, superRegion: Region) -> bool:
|
| 78 |
+
for name, value in region.items():
|
| 79 |
+
if not name in superRegion:
|
| 80 |
+
return False
|
| 81 |
+
superValue = superRegion[name]
|
| 82 |
+
if isinstance(superValue, (float, int)):
|
| 83 |
+
if value != superValue:
|
| 84 |
+
return False
|
| 85 |
+
else:
|
| 86 |
+
if value not in superValue:
|
| 87 |
+
return False
|
| 88 |
+
return True
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Region:
|
| 92 |
+
designRegion = {}
|
| 93 |
+
for name, value in userRegion.items():
|
| 94 |
+
axis = doc.getAxis(name)
|
| 95 |
+
if axis is None:
|
| 96 |
+
raise DesignSpaceDocumentError(
|
| 97 |
+
f"Cannot find axis named '{name}' for region."
|
| 98 |
+
)
|
| 99 |
+
if isinstance(value, (float, int)):
|
| 100 |
+
designRegion[name] = axis.map_forward(value)
|
| 101 |
+
else:
|
| 102 |
+
designRegion[name] = Range(
|
| 103 |
+
axis.map_forward(value.minimum),
|
| 104 |
+
axis.map_forward(value.maximum),
|
| 105 |
+
axis.map_forward(value.default),
|
| 106 |
+
)
|
| 107 |
+
return designRegion
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region:
|
| 111 |
+
vfUserRegion: Region = {}
|
| 112 |
+
# For each axis, 2 cases:
|
| 113 |
+
# - it has a range = it's an axis in the VF DS
|
| 114 |
+
# - it's a single location = use it to know which rules should apply in the VF
|
| 115 |
+
for axisSubset in vf.axisSubsets:
|
| 116 |
+
axis = doc.getAxis(axisSubset.name)
|
| 117 |
+
if axis is None:
|
| 118 |
+
raise DesignSpaceDocumentError(
|
| 119 |
+
f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'."
|
| 120 |
+
)
|
| 121 |
+
if hasattr(axisSubset, "userMinimum"):
|
| 122 |
+
# Mypy doesn't support narrowing union types via hasattr()
|
| 123 |
+
# TODO(Python 3.10): use TypeGuard
|
| 124 |
+
# https://mypy.readthedocs.io/en/stable/type_narrowing.html
|
| 125 |
+
axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset)
|
| 126 |
+
if not hasattr(axis, "minimum"):
|
| 127 |
+
raise DesignSpaceDocumentError(
|
| 128 |
+
f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' "
|
| 129 |
+
"because it's a discrete axis, use only 'userValue' instead."
|
| 130 |
+
)
|
| 131 |
+
axis = cast(AxisDescriptor, axis)
|
| 132 |
+
vfUserRegion[axis.name] = Range(
|
| 133 |
+
max(axisSubset.userMinimum, axis.minimum),
|
| 134 |
+
min(axisSubset.userMaximum, axis.maximum),
|
| 135 |
+
axisSubset.userDefault or axis.default,
|
| 136 |
+
)
|
| 137 |
+
else:
|
| 138 |
+
axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset)
|
| 139 |
+
vfUserRegion[axis.name] = axisSubset.userValue
|
| 140 |
+
# Any axis not mentioned explicitly has a single location = default value
|
| 141 |
+
for axis in doc.axes:
|
| 142 |
+
if axis.name not in vfUserRegion:
|
| 143 |
+
assert isinstance(
|
| 144 |
+
axis.default, (int, float)
|
| 145 |
+
), f"Axis '{axis.name}' has no valid default value."
|
| 146 |
+
vfUserRegion[axis.name] = axis.default
|
| 147 |
+
return vfUserRegion
|
lib/python3.12/site-packages/fontTools/encodings/MacRoman.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MacRoman = [
|
| 2 |
+
"NUL",
|
| 3 |
+
"Eth",
|
| 4 |
+
"eth",
|
| 5 |
+
"Lslash",
|
| 6 |
+
"lslash",
|
| 7 |
+
"Scaron",
|
| 8 |
+
"scaron",
|
| 9 |
+
"Yacute",
|
| 10 |
+
"yacute",
|
| 11 |
+
"HT",
|
| 12 |
+
"LF",
|
| 13 |
+
"Thorn",
|
| 14 |
+
"thorn",
|
| 15 |
+
"CR",
|
| 16 |
+
"Zcaron",
|
| 17 |
+
"zcaron",
|
| 18 |
+
"DLE",
|
| 19 |
+
"DC1",
|
| 20 |
+
"DC2",
|
| 21 |
+
"DC3",
|
| 22 |
+
"DC4",
|
| 23 |
+
"onehalf",
|
| 24 |
+
"onequarter",
|
| 25 |
+
"onesuperior",
|
| 26 |
+
"threequarters",
|
| 27 |
+
"threesuperior",
|
| 28 |
+
"twosuperior",
|
| 29 |
+
"brokenbar",
|
| 30 |
+
"minus",
|
| 31 |
+
"multiply",
|
| 32 |
+
"RS",
|
| 33 |
+
"US",
|
| 34 |
+
"space",
|
| 35 |
+
"exclam",
|
| 36 |
+
"quotedbl",
|
| 37 |
+
"numbersign",
|
| 38 |
+
"dollar",
|
| 39 |
+
"percent",
|
| 40 |
+
"ampersand",
|
| 41 |
+
"quotesingle",
|
| 42 |
+
"parenleft",
|
| 43 |
+
"parenright",
|
| 44 |
+
"asterisk",
|
| 45 |
+
"plus",
|
| 46 |
+
"comma",
|
| 47 |
+
"hyphen",
|
| 48 |
+
"period",
|
| 49 |
+
"slash",
|
| 50 |
+
"zero",
|
| 51 |
+
"one",
|
| 52 |
+
"two",
|
| 53 |
+
"three",
|
| 54 |
+
"four",
|
| 55 |
+
"five",
|
| 56 |
+
"six",
|
| 57 |
+
"seven",
|
| 58 |
+
"eight",
|
| 59 |
+
"nine",
|
| 60 |
+
"colon",
|
| 61 |
+
"semicolon",
|
| 62 |
+
"less",
|
| 63 |
+
"equal",
|
| 64 |
+
"greater",
|
| 65 |
+
"question",
|
| 66 |
+
"at",
|
| 67 |
+
"A",
|
| 68 |
+
"B",
|
| 69 |
+
"C",
|
| 70 |
+
"D",
|
| 71 |
+
"E",
|
| 72 |
+
"F",
|
| 73 |
+
"G",
|
| 74 |
+
"H",
|
| 75 |
+
"I",
|
| 76 |
+
"J",
|
| 77 |
+
"K",
|
| 78 |
+
"L",
|
| 79 |
+
"M",
|
| 80 |
+
"N",
|
| 81 |
+
"O",
|
| 82 |
+
"P",
|
| 83 |
+
"Q",
|
| 84 |
+
"R",
|
| 85 |
+
"S",
|
| 86 |
+
"T",
|
| 87 |
+
"U",
|
| 88 |
+
"V",
|
| 89 |
+
"W",
|
| 90 |
+
"X",
|
| 91 |
+
"Y",
|
| 92 |
+
"Z",
|
| 93 |
+
"bracketleft",
|
| 94 |
+
"backslash",
|
| 95 |
+
"bracketright",
|
| 96 |
+
"asciicircum",
|
| 97 |
+
"underscore",
|
| 98 |
+
"grave",
|
| 99 |
+
"a",
|
| 100 |
+
"b",
|
| 101 |
+
"c",
|
| 102 |
+
"d",
|
| 103 |
+
"e",
|
| 104 |
+
"f",
|
| 105 |
+
"g",
|
| 106 |
+
"h",
|
| 107 |
+
"i",
|
| 108 |
+
"j",
|
| 109 |
+
"k",
|
| 110 |
+
"l",
|
| 111 |
+
"m",
|
| 112 |
+
"n",
|
| 113 |
+
"o",
|
| 114 |
+
"p",
|
| 115 |
+
"q",
|
| 116 |
+
"r",
|
| 117 |
+
"s",
|
| 118 |
+
"t",
|
| 119 |
+
"u",
|
| 120 |
+
"v",
|
| 121 |
+
"w",
|
| 122 |
+
"x",
|
| 123 |
+
"y",
|
| 124 |
+
"z",
|
| 125 |
+
"braceleft",
|
| 126 |
+
"bar",
|
| 127 |
+
"braceright",
|
| 128 |
+
"asciitilde",
|
| 129 |
+
"DEL",
|
| 130 |
+
"Adieresis",
|
| 131 |
+
"Aring",
|
| 132 |
+
"Ccedilla",
|
| 133 |
+
"Eacute",
|
| 134 |
+
"Ntilde",
|
| 135 |
+
"Odieresis",
|
| 136 |
+
"Udieresis",
|
| 137 |
+
"aacute",
|
| 138 |
+
"agrave",
|
| 139 |
+
"acircumflex",
|
| 140 |
+
"adieresis",
|
| 141 |
+
"atilde",
|
| 142 |
+
"aring",
|
| 143 |
+
"ccedilla",
|
| 144 |
+
"eacute",
|
| 145 |
+
"egrave",
|
| 146 |
+
"ecircumflex",
|
| 147 |
+
"edieresis",
|
| 148 |
+
"iacute",
|
| 149 |
+
"igrave",
|
| 150 |
+
"icircumflex",
|
| 151 |
+
"idieresis",
|
| 152 |
+
"ntilde",
|
| 153 |
+
"oacute",
|
| 154 |
+
"ograve",
|
| 155 |
+
"ocircumflex",
|
| 156 |
+
"odieresis",
|
| 157 |
+
"otilde",
|
| 158 |
+
"uacute",
|
| 159 |
+
"ugrave",
|
| 160 |
+
"ucircumflex",
|
| 161 |
+
"udieresis",
|
| 162 |
+
"dagger",
|
| 163 |
+
"degree",
|
| 164 |
+
"cent",
|
| 165 |
+
"sterling",
|
| 166 |
+
"section",
|
| 167 |
+
"bullet",
|
| 168 |
+
"paragraph",
|
| 169 |
+
"germandbls",
|
| 170 |
+
"registered",
|
| 171 |
+
"copyright",
|
| 172 |
+
"trademark",
|
| 173 |
+
"acute",
|
| 174 |
+
"dieresis",
|
| 175 |
+
"notequal",
|
| 176 |
+
"AE",
|
| 177 |
+
"Oslash",
|
| 178 |
+
"infinity",
|
| 179 |
+
"plusminus",
|
| 180 |
+
"lessequal",
|
| 181 |
+
"greaterequal",
|
| 182 |
+
"yen",
|
| 183 |
+
"mu",
|
| 184 |
+
"partialdiff",
|
| 185 |
+
"summation",
|
| 186 |
+
"product",
|
| 187 |
+
"pi",
|
| 188 |
+
"integral",
|
| 189 |
+
"ordfeminine",
|
| 190 |
+
"ordmasculine",
|
| 191 |
+
"Omega",
|
| 192 |
+
"ae",
|
| 193 |
+
"oslash",
|
| 194 |
+
"questiondown",
|
| 195 |
+
"exclamdown",
|
| 196 |
+
"logicalnot",
|
| 197 |
+
"radical",
|
| 198 |
+
"florin",
|
| 199 |
+
"approxequal",
|
| 200 |
+
"Delta",
|
| 201 |
+
"guillemotleft",
|
| 202 |
+
"guillemotright",
|
| 203 |
+
"ellipsis",
|
| 204 |
+
"nbspace",
|
| 205 |
+
"Agrave",
|
| 206 |
+
"Atilde",
|
| 207 |
+
"Otilde",
|
| 208 |
+
"OE",
|
| 209 |
+
"oe",
|
| 210 |
+
"endash",
|
| 211 |
+
"emdash",
|
| 212 |
+
"quotedblleft",
|
| 213 |
+
"quotedblright",
|
| 214 |
+
"quoteleft",
|
| 215 |
+
"quoteright",
|
| 216 |
+
"divide",
|
| 217 |
+
"lozenge",
|
| 218 |
+
"ydieresis",
|
| 219 |
+
"Ydieresis",
|
| 220 |
+
"fraction",
|
| 221 |
+
"currency",
|
| 222 |
+
"guilsinglleft",
|
| 223 |
+
"guilsinglright",
|
| 224 |
+
"fi",
|
| 225 |
+
"fl",
|
| 226 |
+
"daggerdbl",
|
| 227 |
+
"periodcentered",
|
| 228 |
+
"quotesinglbase",
|
| 229 |
+
"quotedblbase",
|
| 230 |
+
"perthousand",
|
| 231 |
+
"Acircumflex",
|
| 232 |
+
"Ecircumflex",
|
| 233 |
+
"Aacute",
|
| 234 |
+
"Edieresis",
|
| 235 |
+
"Egrave",
|
| 236 |
+
"Iacute",
|
| 237 |
+
"Icircumflex",
|
| 238 |
+
"Idieresis",
|
| 239 |
+
"Igrave",
|
| 240 |
+
"Oacute",
|
| 241 |
+
"Ocircumflex",
|
| 242 |
+
"apple",
|
| 243 |
+
"Ograve",
|
| 244 |
+
"Uacute",
|
| 245 |
+
"Ucircumflex",
|
| 246 |
+
"Ugrave",
|
| 247 |
+
"dotlessi",
|
| 248 |
+
"circumflex",
|
| 249 |
+
"tilde",
|
| 250 |
+
"macron",
|
| 251 |
+
"breve",
|
| 252 |
+
"dotaccent",
|
| 253 |
+
"ring",
|
| 254 |
+
"cedilla",
|
| 255 |
+
"hungarumlaut",
|
| 256 |
+
"ogonek",
|
| 257 |
+
"caron",
|
| 258 |
+
]
|
lib/python3.12/site-packages/fontTools/encodings/StandardEncoding.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
StandardEncoding = [
|
| 2 |
+
".notdef",
|
| 3 |
+
".notdef",
|
| 4 |
+
".notdef",
|
| 5 |
+
".notdef",
|
| 6 |
+
".notdef",
|
| 7 |
+
".notdef",
|
| 8 |
+
".notdef",
|
| 9 |
+
".notdef",
|
| 10 |
+
".notdef",
|
| 11 |
+
".notdef",
|
| 12 |
+
".notdef",
|
| 13 |
+
".notdef",
|
| 14 |
+
".notdef",
|
| 15 |
+
".notdef",
|
| 16 |
+
".notdef",
|
| 17 |
+
".notdef",
|
| 18 |
+
".notdef",
|
| 19 |
+
".notdef",
|
| 20 |
+
".notdef",
|
| 21 |
+
".notdef",
|
| 22 |
+
".notdef",
|
| 23 |
+
".notdef",
|
| 24 |
+
".notdef",
|
| 25 |
+
".notdef",
|
| 26 |
+
".notdef",
|
| 27 |
+
".notdef",
|
| 28 |
+
".notdef",
|
| 29 |
+
".notdef",
|
| 30 |
+
".notdef",
|
| 31 |
+
".notdef",
|
| 32 |
+
".notdef",
|
| 33 |
+
".notdef",
|
| 34 |
+
"space",
|
| 35 |
+
"exclam",
|
| 36 |
+
"quotedbl",
|
| 37 |
+
"numbersign",
|
| 38 |
+
"dollar",
|
| 39 |
+
"percent",
|
| 40 |
+
"ampersand",
|
| 41 |
+
"quoteright",
|
| 42 |
+
"parenleft",
|
| 43 |
+
"parenright",
|
| 44 |
+
"asterisk",
|
| 45 |
+
"plus",
|
| 46 |
+
"comma",
|
| 47 |
+
"hyphen",
|
| 48 |
+
"period",
|
| 49 |
+
"slash",
|
| 50 |
+
"zero",
|
| 51 |
+
"one",
|
| 52 |
+
"two",
|
| 53 |
+
"three",
|
| 54 |
+
"four",
|
| 55 |
+
"five",
|
| 56 |
+
"six",
|
| 57 |
+
"seven",
|
| 58 |
+
"eight",
|
| 59 |
+
"nine",
|
| 60 |
+
"colon",
|
| 61 |
+
"semicolon",
|
| 62 |
+
"less",
|
| 63 |
+
"equal",
|
| 64 |
+
"greater",
|
| 65 |
+
"question",
|
| 66 |
+
"at",
|
| 67 |
+
"A",
|
| 68 |
+
"B",
|
| 69 |
+
"C",
|
| 70 |
+
"D",
|
| 71 |
+
"E",
|
| 72 |
+
"F",
|
| 73 |
+
"G",
|
| 74 |
+
"H",
|
| 75 |
+
"I",
|
| 76 |
+
"J",
|
| 77 |
+
"K",
|
| 78 |
+
"L",
|
| 79 |
+
"M",
|
| 80 |
+
"N",
|
| 81 |
+
"O",
|
| 82 |
+
"P",
|
| 83 |
+
"Q",
|
| 84 |
+
"R",
|
| 85 |
+
"S",
|
| 86 |
+
"T",
|
| 87 |
+
"U",
|
| 88 |
+
"V",
|
| 89 |
+
"W",
|
| 90 |
+
"X",
|
| 91 |
+
"Y",
|
| 92 |
+
"Z",
|
| 93 |
+
"bracketleft",
|
| 94 |
+
"backslash",
|
| 95 |
+
"bracketright",
|
| 96 |
+
"asciicircum",
|
| 97 |
+
"underscore",
|
| 98 |
+
"quoteleft",
|
| 99 |
+
"a",
|
| 100 |
+
"b",
|
| 101 |
+
"c",
|
| 102 |
+
"d",
|
| 103 |
+
"e",
|
| 104 |
+
"f",
|
| 105 |
+
"g",
|
| 106 |
+
"h",
|
| 107 |
+
"i",
|
| 108 |
+
"j",
|
| 109 |
+
"k",
|
| 110 |
+
"l",
|
| 111 |
+
"m",
|
| 112 |
+
"n",
|
| 113 |
+
"o",
|
| 114 |
+
"p",
|
| 115 |
+
"q",
|
| 116 |
+
"r",
|
| 117 |
+
"s",
|
| 118 |
+
"t",
|
| 119 |
+
"u",
|
| 120 |
+
"v",
|
| 121 |
+
"w",
|
| 122 |
+
"x",
|
| 123 |
+
"y",
|
| 124 |
+
"z",
|
| 125 |
+
"braceleft",
|
| 126 |
+
"bar",
|
| 127 |
+
"braceright",
|
| 128 |
+
"asciitilde",
|
| 129 |
+
".notdef",
|
| 130 |
+
".notdef",
|
| 131 |
+
".notdef",
|
| 132 |
+
".notdef",
|
| 133 |
+
".notdef",
|
| 134 |
+
".notdef",
|
| 135 |
+
".notdef",
|
| 136 |
+
".notdef",
|
| 137 |
+
".notdef",
|
| 138 |
+
".notdef",
|
| 139 |
+
".notdef",
|
| 140 |
+
".notdef",
|
| 141 |
+
".notdef",
|
| 142 |
+
".notdef",
|
| 143 |
+
".notdef",
|
| 144 |
+
".notdef",
|
| 145 |
+
".notdef",
|
| 146 |
+
".notdef",
|
| 147 |
+
".notdef",
|
| 148 |
+
".notdef",
|
| 149 |
+
".notdef",
|
| 150 |
+
".notdef",
|
| 151 |
+
".notdef",
|
| 152 |
+
".notdef",
|
| 153 |
+
".notdef",
|
| 154 |
+
".notdef",
|
| 155 |
+
".notdef",
|
| 156 |
+
".notdef",
|
| 157 |
+
".notdef",
|
| 158 |
+
".notdef",
|
| 159 |
+
".notdef",
|
| 160 |
+
".notdef",
|
| 161 |
+
".notdef",
|
| 162 |
+
".notdef",
|
| 163 |
+
"exclamdown",
|
| 164 |
+
"cent",
|
| 165 |
+
"sterling",
|
| 166 |
+
"fraction",
|
| 167 |
+
"yen",
|
| 168 |
+
"florin",
|
| 169 |
+
"section",
|
| 170 |
+
"currency",
|
| 171 |
+
"quotesingle",
|
| 172 |
+
"quotedblleft",
|
| 173 |
+
"guillemotleft",
|
| 174 |
+
"guilsinglleft",
|
| 175 |
+
"guilsinglright",
|
| 176 |
+
"fi",
|
| 177 |
+
"fl",
|
| 178 |
+
".notdef",
|
| 179 |
+
"endash",
|
| 180 |
+
"dagger",
|
| 181 |
+
"daggerdbl",
|
| 182 |
+
"periodcentered",
|
| 183 |
+
".notdef",
|
| 184 |
+
"paragraph",
|
| 185 |
+
"bullet",
|
| 186 |
+
"quotesinglbase",
|
| 187 |
+
"quotedblbase",
|
| 188 |
+
"quotedblright",
|
| 189 |
+
"guillemotright",
|
| 190 |
+
"ellipsis",
|
| 191 |
+
"perthousand",
|
| 192 |
+
".notdef",
|
| 193 |
+
"questiondown",
|
| 194 |
+
".notdef",
|
| 195 |
+
"grave",
|
| 196 |
+
"acute",
|
| 197 |
+
"circumflex",
|
| 198 |
+
"tilde",
|
| 199 |
+
"macron",
|
| 200 |
+
"breve",
|
| 201 |
+
"dotaccent",
|
| 202 |
+
"dieresis",
|
| 203 |
+
".notdef",
|
| 204 |
+
"ring",
|
| 205 |
+
"cedilla",
|
| 206 |
+
".notdef",
|
| 207 |
+
"hungarumlaut",
|
| 208 |
+
"ogonek",
|
| 209 |
+
"caron",
|
| 210 |
+
"emdash",
|
| 211 |
+
".notdef",
|
| 212 |
+
".notdef",
|
| 213 |
+
".notdef",
|
| 214 |
+
".notdef",
|
| 215 |
+
".notdef",
|
| 216 |
+
".notdef",
|
| 217 |
+
".notdef",
|
| 218 |
+
".notdef",
|
| 219 |
+
".notdef",
|
| 220 |
+
".notdef",
|
| 221 |
+
".notdef",
|
| 222 |
+
".notdef",
|
| 223 |
+
".notdef",
|
| 224 |
+
".notdef",
|
| 225 |
+
".notdef",
|
| 226 |
+
".notdef",
|
| 227 |
+
"AE",
|
| 228 |
+
".notdef",
|
| 229 |
+
"ordfeminine",
|
| 230 |
+
".notdef",
|
| 231 |
+
".notdef",
|
| 232 |
+
".notdef",
|
| 233 |
+
".notdef",
|
| 234 |
+
"Lslash",
|
| 235 |
+
"Oslash",
|
| 236 |
+
"OE",
|
| 237 |
+
"ordmasculine",
|
| 238 |
+
".notdef",
|
| 239 |
+
".notdef",
|
| 240 |
+
".notdef",
|
| 241 |
+
".notdef",
|
| 242 |
+
".notdef",
|
| 243 |
+
"ae",
|
| 244 |
+
".notdef",
|
| 245 |
+
".notdef",
|
| 246 |
+
".notdef",
|
| 247 |
+
"dotlessi",
|
| 248 |
+
".notdef",
|
| 249 |
+
".notdef",
|
| 250 |
+
"lslash",
|
| 251 |
+
"oslash",
|
| 252 |
+
"oe",
|
| 253 |
+
"germandbls",
|
| 254 |
+
".notdef",
|
| 255 |
+
".notdef",
|
| 256 |
+
".notdef",
|
| 257 |
+
".notdef",
|
| 258 |
+
]
|
lib/python3.12/site-packages/fontTools/encodings/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Empty __init__.py file to signal Python this directory is a package."""
|
lib/python3.12/site-packages/fontTools/encodings/__pycache__/MacRoman.cpython-312.pyc
ADDED
|
Binary file (2.25 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/encodings/__pycache__/StandardEncoding.cpython-312.pyc
ADDED
|
Binary file (1.84 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/encodings/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (281 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/encodings/__pycache__/codecs.cpython-312.pyc
ADDED
|
Binary file (5.74 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/encodings/codecs.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Extend the Python codecs module with a few encodings that are used in OpenType (name table)
|
| 2 |
+
but missing from Python. See https://github.com/fonttools/fonttools/issues/236 for details."""
|
| 3 |
+
|
| 4 |
+
import codecs
|
| 5 |
+
import encodings
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ExtendCodec(codecs.Codec):
|
| 9 |
+
def __init__(self, name, base_encoding, mapping):
|
| 10 |
+
self.name = name
|
| 11 |
+
self.base_encoding = base_encoding
|
| 12 |
+
self.mapping = mapping
|
| 13 |
+
self.reverse = {v: k for k, v in mapping.items()}
|
| 14 |
+
self.max_len = max(len(v) for v in mapping.values())
|
| 15 |
+
self.info = codecs.CodecInfo(
|
| 16 |
+
name=self.name, encode=self.encode, decode=self.decode
|
| 17 |
+
)
|
| 18 |
+
codecs.register_error(name, self.error)
|
| 19 |
+
|
| 20 |
+
def _map(self, mapper, output_type, exc_type, input, errors):
|
| 21 |
+
base_error_handler = codecs.lookup_error(errors)
|
| 22 |
+
length = len(input)
|
| 23 |
+
out = output_type()
|
| 24 |
+
while input:
|
| 25 |
+
# first try to use self.error as the error handler
|
| 26 |
+
try:
|
| 27 |
+
part = mapper(input, self.base_encoding, errors=self.name)
|
| 28 |
+
out += part
|
| 29 |
+
break # All converted
|
| 30 |
+
except exc_type as e:
|
| 31 |
+
# else convert the correct part, handle error as requested and continue
|
| 32 |
+
out += mapper(input[: e.start], self.base_encoding, self.name)
|
| 33 |
+
replacement, pos = base_error_handler(e)
|
| 34 |
+
out += replacement
|
| 35 |
+
input = input[pos:]
|
| 36 |
+
return out, length
|
| 37 |
+
|
| 38 |
+
def encode(self, input, errors="strict"):
|
| 39 |
+
return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
|
| 40 |
+
|
| 41 |
+
def decode(self, input, errors="strict"):
|
| 42 |
+
return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
|
| 43 |
+
|
| 44 |
+
def error(self, e):
|
| 45 |
+
if isinstance(e, UnicodeDecodeError):
|
| 46 |
+
for end in range(e.start + 1, e.end + 1):
|
| 47 |
+
s = e.object[e.start : end]
|
| 48 |
+
if s in self.mapping:
|
| 49 |
+
return self.mapping[s], end
|
| 50 |
+
elif isinstance(e, UnicodeEncodeError):
|
| 51 |
+
for end in range(e.start + 1, e.start + self.max_len + 1):
|
| 52 |
+
s = e.object[e.start : end]
|
| 53 |
+
if s in self.reverse:
|
| 54 |
+
return self.reverse[s], end
|
| 55 |
+
e.encoding = self.name
|
| 56 |
+
raise e
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
_extended_encodings = {
|
| 60 |
+
"x_mac_japanese_ttx": (
|
| 61 |
+
"shift_jis",
|
| 62 |
+
{
|
| 63 |
+
b"\xFC": chr(0x007C),
|
| 64 |
+
b"\x7E": chr(0x007E),
|
| 65 |
+
b"\x80": chr(0x005C),
|
| 66 |
+
b"\xA0": chr(0x00A0),
|
| 67 |
+
b"\xFD": chr(0x00A9),
|
| 68 |
+
b"\xFE": chr(0x2122),
|
| 69 |
+
b"\xFF": chr(0x2026),
|
| 70 |
+
},
|
| 71 |
+
),
|
| 72 |
+
"x_mac_trad_chinese_ttx": (
|
| 73 |
+
"big5",
|
| 74 |
+
{
|
| 75 |
+
b"\x80": chr(0x005C),
|
| 76 |
+
b"\xA0": chr(0x00A0),
|
| 77 |
+
b"\xFD": chr(0x00A9),
|
| 78 |
+
b"\xFE": chr(0x2122),
|
| 79 |
+
b"\xFF": chr(0x2026),
|
| 80 |
+
},
|
| 81 |
+
),
|
| 82 |
+
"x_mac_korean_ttx": (
|
| 83 |
+
"euc_kr",
|
| 84 |
+
{
|
| 85 |
+
b"\x80": chr(0x00A0),
|
| 86 |
+
b"\x81": chr(0x20A9),
|
| 87 |
+
b"\x82": chr(0x2014),
|
| 88 |
+
b"\x83": chr(0x00A9),
|
| 89 |
+
b"\xFE": chr(0x2122),
|
| 90 |
+
b"\xFF": chr(0x2026),
|
| 91 |
+
},
|
| 92 |
+
),
|
| 93 |
+
"x_mac_simp_chinese_ttx": (
|
| 94 |
+
"gb2312",
|
| 95 |
+
{
|
| 96 |
+
b"\x80": chr(0x00FC),
|
| 97 |
+
b"\xA0": chr(0x00A0),
|
| 98 |
+
b"\xFD": chr(0x00A9),
|
| 99 |
+
b"\xFE": chr(0x2122),
|
| 100 |
+
b"\xFF": chr(0x2026),
|
| 101 |
+
},
|
| 102 |
+
),
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
_cache = {}
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def search_function(name):
|
| 109 |
+
name = encodings.normalize_encoding(name) # Rather undocumented...
|
| 110 |
+
if name in _extended_encodings:
|
| 111 |
+
if name not in _cache:
|
| 112 |
+
base_encoding, mapping = _extended_encodings[name]
|
| 113 |
+
assert name[-4:] == "_ttx"
|
| 114 |
+
# Python 2 didn't have any of the encodings that we are implementing
|
| 115 |
+
# in this file. Python 3 added aliases for the East Asian ones, mapping
|
| 116 |
+
# them "temporarily" to the same base encoding as us, with a comment
|
| 117 |
+
# suggesting that full implementation will appear some time later.
|
| 118 |
+
# As such, try the Python version of the x_mac_... first, if that is found,
|
| 119 |
+
# use *that* as our base encoding. This would make our encoding upgrade
|
| 120 |
+
# to the full encoding when and if Python finally implements that.
|
| 121 |
+
# http://bugs.python.org/issue24041
|
| 122 |
+
base_encodings = [name[:-4], base_encoding]
|
| 123 |
+
for base_encoding in base_encodings:
|
| 124 |
+
try:
|
| 125 |
+
codecs.lookup(base_encoding)
|
| 126 |
+
except LookupError:
|
| 127 |
+
continue
|
| 128 |
+
_cache[name] = ExtendCodec(name, base_encoding, mapping)
|
| 129 |
+
break
|
| 130 |
+
return _cache[name].info
|
| 131 |
+
|
| 132 |
+
return None
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
codecs.register(search_function)
|
lib/python3.12/site-packages/fontTools/fontBuilder.py
ADDED
|
@@ -0,0 +1,1014 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__all__ = ["FontBuilder"]
|
| 2 |
+
|
| 3 |
+
"""
|
| 4 |
+
This module is *experimental*, meaning it still may evolve and change.
|
| 5 |
+
|
| 6 |
+
The `FontBuilder` class is a convenient helper to construct working TTF or
|
| 7 |
+
OTF fonts from scratch.
|
| 8 |
+
|
| 9 |
+
Note that the various setup methods cannot be called in arbitrary order,
|
| 10 |
+
due to various interdependencies between OpenType tables. Here is an order
|
| 11 |
+
that works:
|
| 12 |
+
|
| 13 |
+
fb = FontBuilder(...)
|
| 14 |
+
fb.setupGlyphOrder(...)
|
| 15 |
+
fb.setupCharacterMap(...)
|
| 16 |
+
fb.setupGlyf(...) --or-- fb.setupCFF(...)
|
| 17 |
+
fb.setupHorizontalMetrics(...)
|
| 18 |
+
fb.setupHorizontalHeader()
|
| 19 |
+
fb.setupNameTable(...)
|
| 20 |
+
fb.setupOS2()
|
| 21 |
+
fb.addOpenTypeFeatures(...)
|
| 22 |
+
fb.setupPost()
|
| 23 |
+
fb.save(...)
|
| 24 |
+
|
| 25 |
+
Here is how to build a minimal TTF:
|
| 26 |
+
|
| 27 |
+
```python
|
| 28 |
+
from fontTools.fontBuilder import FontBuilder
|
| 29 |
+
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def drawTestGlyph(pen):
|
| 33 |
+
pen.moveTo((100, 100))
|
| 34 |
+
pen.lineTo((100, 1000))
|
| 35 |
+
pen.qCurveTo((200, 900), (400, 900), (500, 1000))
|
| 36 |
+
pen.lineTo((500, 100))
|
| 37 |
+
pen.closePath()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
fb = FontBuilder(1024, isTTF=True)
|
| 41 |
+
fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
|
| 42 |
+
fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
|
| 43 |
+
advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
|
| 44 |
+
|
| 45 |
+
familyName = "HelloTestFont"
|
| 46 |
+
styleName = "TotallyNormal"
|
| 47 |
+
version = "0.1"
|
| 48 |
+
|
| 49 |
+
nameStrings = dict(
|
| 50 |
+
familyName=dict(en=familyName, nl="HalloTestFont"),
|
| 51 |
+
styleName=dict(en=styleName, nl="TotaalNormaal"),
|
| 52 |
+
uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
|
| 53 |
+
fullName=familyName + "-" + styleName,
|
| 54 |
+
psName=familyName + "-" + styleName,
|
| 55 |
+
version="Version " + version,
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
pen = TTGlyphPen(None)
|
| 59 |
+
drawTestGlyph(pen)
|
| 60 |
+
glyph = pen.glyph()
|
| 61 |
+
glyphs = {".notdef": glyph, "space": glyph, "A": glyph, "a": glyph, ".null": glyph}
|
| 62 |
+
fb.setupGlyf(glyphs)
|
| 63 |
+
metrics = {}
|
| 64 |
+
glyphTable = fb.font["glyf"]
|
| 65 |
+
for gn, advanceWidth in advanceWidths.items():
|
| 66 |
+
metrics[gn] = (advanceWidth, glyphTable[gn].xMin)
|
| 67 |
+
fb.setupHorizontalMetrics(metrics)
|
| 68 |
+
fb.setupHorizontalHeader(ascent=824, descent=-200)
|
| 69 |
+
fb.setupNameTable(nameStrings)
|
| 70 |
+
fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
|
| 71 |
+
fb.setupPost()
|
| 72 |
+
fb.save("test.ttf")
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
And here's how to build a minimal OTF:
|
| 76 |
+
|
| 77 |
+
```python
|
| 78 |
+
from fontTools.fontBuilder import FontBuilder
|
| 79 |
+
from fontTools.pens.t2CharStringPen import T2CharStringPen
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def drawTestGlyph(pen):
|
| 83 |
+
pen.moveTo((100, 100))
|
| 84 |
+
pen.lineTo((100, 1000))
|
| 85 |
+
pen.curveTo((200, 900), (400, 900), (500, 1000))
|
| 86 |
+
pen.lineTo((500, 100))
|
| 87 |
+
pen.closePath()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
fb = FontBuilder(1024, isTTF=False)
|
| 91 |
+
fb.setupGlyphOrder([".notdef", ".null", "space", "A", "a"])
|
| 92 |
+
fb.setupCharacterMap({32: "space", 65: "A", 97: "a"})
|
| 93 |
+
advanceWidths = {".notdef": 600, "space": 500, "A": 600, "a": 600, ".null": 0}
|
| 94 |
+
|
| 95 |
+
familyName = "HelloTestFont"
|
| 96 |
+
styleName = "TotallyNormal"
|
| 97 |
+
version = "0.1"
|
| 98 |
+
|
| 99 |
+
nameStrings = dict(
|
| 100 |
+
familyName=dict(en=familyName, nl="HalloTestFont"),
|
| 101 |
+
styleName=dict(en=styleName, nl="TotaalNormaal"),
|
| 102 |
+
uniqueFontIdentifier="fontBuilder: " + familyName + "." + styleName,
|
| 103 |
+
fullName=familyName + "-" + styleName,
|
| 104 |
+
psName=familyName + "-" + styleName,
|
| 105 |
+
version="Version " + version,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
pen = T2CharStringPen(600, None)
|
| 109 |
+
drawTestGlyph(pen)
|
| 110 |
+
charString = pen.getCharString()
|
| 111 |
+
charStrings = {
|
| 112 |
+
".notdef": charString,
|
| 113 |
+
"space": charString,
|
| 114 |
+
"A": charString,
|
| 115 |
+
"a": charString,
|
| 116 |
+
".null": charString,
|
| 117 |
+
}
|
| 118 |
+
fb.setupCFF(nameStrings["psName"], {"FullName": nameStrings["psName"]}, charStrings, {})
|
| 119 |
+
lsb = {gn: cs.calcBounds(None)[0] for gn, cs in charStrings.items()}
|
| 120 |
+
metrics = {}
|
| 121 |
+
for gn, advanceWidth in advanceWidths.items():
|
| 122 |
+
metrics[gn] = (advanceWidth, lsb[gn])
|
| 123 |
+
fb.setupHorizontalMetrics(metrics)
|
| 124 |
+
fb.setupHorizontalHeader(ascent=824, descent=200)
|
| 125 |
+
fb.setupNameTable(nameStrings)
|
| 126 |
+
fb.setupOS2(sTypoAscender=824, usWinAscent=824, usWinDescent=200)
|
| 127 |
+
fb.setupPost()
|
| 128 |
+
fb.save("test.otf")
|
| 129 |
+
```
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
from .ttLib import TTFont, newTable
|
| 133 |
+
from .ttLib.tables._c_m_a_p import cmap_classes
|
| 134 |
+
from .ttLib.tables._g_l_y_f import flagCubic
|
| 135 |
+
from .ttLib.tables.O_S_2f_2 import Panose
|
| 136 |
+
from .misc.timeTools import timestampNow
|
| 137 |
+
import struct
|
| 138 |
+
from collections import OrderedDict
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
_headDefaults = dict(
|
| 142 |
+
tableVersion=1.0,
|
| 143 |
+
fontRevision=1.0,
|
| 144 |
+
checkSumAdjustment=0,
|
| 145 |
+
magicNumber=0x5F0F3CF5,
|
| 146 |
+
flags=0x0003,
|
| 147 |
+
unitsPerEm=1000,
|
| 148 |
+
created=0,
|
| 149 |
+
modified=0,
|
| 150 |
+
xMin=0,
|
| 151 |
+
yMin=0,
|
| 152 |
+
xMax=0,
|
| 153 |
+
yMax=0,
|
| 154 |
+
macStyle=0,
|
| 155 |
+
lowestRecPPEM=3,
|
| 156 |
+
fontDirectionHint=2,
|
| 157 |
+
indexToLocFormat=0,
|
| 158 |
+
glyphDataFormat=0,
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
_maxpDefaultsTTF = dict(
|
| 162 |
+
tableVersion=0x00010000,
|
| 163 |
+
numGlyphs=0,
|
| 164 |
+
maxPoints=0,
|
| 165 |
+
maxContours=0,
|
| 166 |
+
maxCompositePoints=0,
|
| 167 |
+
maxCompositeContours=0,
|
| 168 |
+
maxZones=2,
|
| 169 |
+
maxTwilightPoints=0,
|
| 170 |
+
maxStorage=0,
|
| 171 |
+
maxFunctionDefs=0,
|
| 172 |
+
maxInstructionDefs=0,
|
| 173 |
+
maxStackElements=0,
|
| 174 |
+
maxSizeOfInstructions=0,
|
| 175 |
+
maxComponentElements=0,
|
| 176 |
+
maxComponentDepth=0,
|
| 177 |
+
)
|
| 178 |
+
_maxpDefaultsOTF = dict(
|
| 179 |
+
tableVersion=0x00005000,
|
| 180 |
+
numGlyphs=0,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
_postDefaults = dict(
|
| 184 |
+
formatType=3.0,
|
| 185 |
+
italicAngle=0,
|
| 186 |
+
underlinePosition=0,
|
| 187 |
+
underlineThickness=0,
|
| 188 |
+
isFixedPitch=0,
|
| 189 |
+
minMemType42=0,
|
| 190 |
+
maxMemType42=0,
|
| 191 |
+
minMemType1=0,
|
| 192 |
+
maxMemType1=0,
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
_hheaDefaults = dict(
|
| 196 |
+
tableVersion=0x00010000,
|
| 197 |
+
ascent=0,
|
| 198 |
+
descent=0,
|
| 199 |
+
lineGap=0,
|
| 200 |
+
advanceWidthMax=0,
|
| 201 |
+
minLeftSideBearing=0,
|
| 202 |
+
minRightSideBearing=0,
|
| 203 |
+
xMaxExtent=0,
|
| 204 |
+
caretSlopeRise=1,
|
| 205 |
+
caretSlopeRun=0,
|
| 206 |
+
caretOffset=0,
|
| 207 |
+
reserved0=0,
|
| 208 |
+
reserved1=0,
|
| 209 |
+
reserved2=0,
|
| 210 |
+
reserved3=0,
|
| 211 |
+
metricDataFormat=0,
|
| 212 |
+
numberOfHMetrics=0,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
_vheaDefaults = dict(
|
| 216 |
+
tableVersion=0x00010000,
|
| 217 |
+
ascent=0,
|
| 218 |
+
descent=0,
|
| 219 |
+
lineGap=0,
|
| 220 |
+
advanceHeightMax=0,
|
| 221 |
+
minTopSideBearing=0,
|
| 222 |
+
minBottomSideBearing=0,
|
| 223 |
+
yMaxExtent=0,
|
| 224 |
+
caretSlopeRise=0,
|
| 225 |
+
caretSlopeRun=0,
|
| 226 |
+
reserved0=0,
|
| 227 |
+
reserved1=0,
|
| 228 |
+
reserved2=0,
|
| 229 |
+
reserved3=0,
|
| 230 |
+
reserved4=0,
|
| 231 |
+
metricDataFormat=0,
|
| 232 |
+
numberOfVMetrics=0,
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
_nameIDs = dict(
|
| 236 |
+
copyright=0,
|
| 237 |
+
familyName=1,
|
| 238 |
+
styleName=2,
|
| 239 |
+
uniqueFontIdentifier=3,
|
| 240 |
+
fullName=4,
|
| 241 |
+
version=5,
|
| 242 |
+
psName=6,
|
| 243 |
+
trademark=7,
|
| 244 |
+
manufacturer=8,
|
| 245 |
+
designer=9,
|
| 246 |
+
description=10,
|
| 247 |
+
vendorURL=11,
|
| 248 |
+
designerURL=12,
|
| 249 |
+
licenseDescription=13,
|
| 250 |
+
licenseInfoURL=14,
|
| 251 |
+
# reserved = 15,
|
| 252 |
+
typographicFamily=16,
|
| 253 |
+
typographicSubfamily=17,
|
| 254 |
+
compatibleFullName=18,
|
| 255 |
+
sampleText=19,
|
| 256 |
+
postScriptCIDFindfontName=20,
|
| 257 |
+
wwsFamilyName=21,
|
| 258 |
+
wwsSubfamilyName=22,
|
| 259 |
+
lightBackgroundPalette=23,
|
| 260 |
+
darkBackgroundPalette=24,
|
| 261 |
+
variationsPostScriptNamePrefix=25,
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
# to insert in setupNameTable doc string:
|
| 265 |
+
# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def _getOS2Defaults():
|
| 269 |
+
return dict(
|
| 270 |
+
version=3,
|
| 271 |
+
xAvgCharWidth=0,
|
| 272 |
+
usWeightClass=400,
|
| 273 |
+
usWidthClass=5,
|
| 274 |
+
fsType=0x0004, # default: Preview & Print embedding
|
| 275 |
+
ySubscriptXSize=0,
|
| 276 |
+
ySubscriptYSize=0,
|
| 277 |
+
ySubscriptXOffset=0,
|
| 278 |
+
ySubscriptYOffset=0,
|
| 279 |
+
ySuperscriptXSize=0,
|
| 280 |
+
ySuperscriptYSize=0,
|
| 281 |
+
ySuperscriptXOffset=0,
|
| 282 |
+
ySuperscriptYOffset=0,
|
| 283 |
+
yStrikeoutSize=0,
|
| 284 |
+
yStrikeoutPosition=0,
|
| 285 |
+
sFamilyClass=0,
|
| 286 |
+
panose=Panose(),
|
| 287 |
+
ulUnicodeRange1=0,
|
| 288 |
+
ulUnicodeRange2=0,
|
| 289 |
+
ulUnicodeRange3=0,
|
| 290 |
+
ulUnicodeRange4=0,
|
| 291 |
+
achVendID="????",
|
| 292 |
+
fsSelection=0,
|
| 293 |
+
usFirstCharIndex=0,
|
| 294 |
+
usLastCharIndex=0,
|
| 295 |
+
sTypoAscender=0,
|
| 296 |
+
sTypoDescender=0,
|
| 297 |
+
sTypoLineGap=0,
|
| 298 |
+
usWinAscent=0,
|
| 299 |
+
usWinDescent=0,
|
| 300 |
+
ulCodePageRange1=0,
|
| 301 |
+
ulCodePageRange2=0,
|
| 302 |
+
sxHeight=0,
|
| 303 |
+
sCapHeight=0,
|
| 304 |
+
usDefaultChar=0, # .notdef
|
| 305 |
+
usBreakChar=32, # space
|
| 306 |
+
usMaxContext=0,
|
| 307 |
+
usLowerOpticalPointSize=0,
|
| 308 |
+
usUpperOpticalPointSize=0,
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
class FontBuilder(object):
|
| 313 |
+
def __init__(self, unitsPerEm=None, font=None, isTTF=True, glyphDataFormat=0):
|
| 314 |
+
"""Initialize a FontBuilder instance.
|
| 315 |
+
|
| 316 |
+
If the `font` argument is not given, a new `TTFont` will be
|
| 317 |
+
constructed, and `unitsPerEm` must be given. If `isTTF` is True,
|
| 318 |
+
the font will be a glyf-based TTF; if `isTTF` is False it will be
|
| 319 |
+
a CFF-based OTF.
|
| 320 |
+
|
| 321 |
+
The `glyphDataFormat` argument corresponds to the `head` table field
|
| 322 |
+
that defines the format of the TrueType `glyf` table (default=0).
|
| 323 |
+
TrueType glyphs historically can only contain quadratic splines and static
|
| 324 |
+
components, but there's a proposal to add support for cubic Bezier curves as well
|
| 325 |
+
as variable composites/components at
|
| 326 |
+
https://github.com/harfbuzz/boring-expansion-spec/blob/main/glyf1.md
|
| 327 |
+
You can experiment with the new features by setting `glyphDataFormat` to 1.
|
| 328 |
+
A ValueError is raised if `glyphDataFormat` is left at 0 but glyphs are added
|
| 329 |
+
that contain cubic splines or varcomposites. This is to prevent accidentally
|
| 330 |
+
creating fonts that are incompatible with existing TrueType implementations.
|
| 331 |
+
|
| 332 |
+
If `font` is given, it must be a `TTFont` instance and `unitsPerEm`
|
| 333 |
+
must _not_ be given. The `isTTF` and `glyphDataFormat` arguments will be ignored.
|
| 334 |
+
"""
|
| 335 |
+
if font is None:
|
| 336 |
+
self.font = TTFont(recalcTimestamp=False)
|
| 337 |
+
self.isTTF = isTTF
|
| 338 |
+
now = timestampNow()
|
| 339 |
+
assert unitsPerEm is not None
|
| 340 |
+
self.setupHead(
|
| 341 |
+
unitsPerEm=unitsPerEm,
|
| 342 |
+
created=now,
|
| 343 |
+
modified=now,
|
| 344 |
+
glyphDataFormat=glyphDataFormat,
|
| 345 |
+
)
|
| 346 |
+
self.setupMaxp()
|
| 347 |
+
else:
|
| 348 |
+
assert unitsPerEm is None
|
| 349 |
+
self.font = font
|
| 350 |
+
self.isTTF = "glyf" in font
|
| 351 |
+
|
| 352 |
+
def save(self, file):
|
| 353 |
+
"""Save the font. The 'file' argument can be either a pathname or a
|
| 354 |
+
writable file object.
|
| 355 |
+
"""
|
| 356 |
+
self.font.save(file)
|
| 357 |
+
|
| 358 |
+
def _initTableWithValues(self, tableTag, defaults, values):
|
| 359 |
+
table = self.font[tableTag] = newTable(tableTag)
|
| 360 |
+
for k, v in defaults.items():
|
| 361 |
+
setattr(table, k, v)
|
| 362 |
+
for k, v in values.items():
|
| 363 |
+
setattr(table, k, v)
|
| 364 |
+
return table
|
| 365 |
+
|
| 366 |
+
def _updateTableWithValues(self, tableTag, values):
|
| 367 |
+
table = self.font[tableTag]
|
| 368 |
+
for k, v in values.items():
|
| 369 |
+
setattr(table, k, v)
|
| 370 |
+
|
| 371 |
+
def setupHead(self, **values):
|
| 372 |
+
"""Create a new `head` table and initialize it with default values,
|
| 373 |
+
which can be overridden by keyword arguments.
|
| 374 |
+
"""
|
| 375 |
+
self._initTableWithValues("head", _headDefaults, values)
|
| 376 |
+
|
| 377 |
+
def updateHead(self, **values):
|
| 378 |
+
"""Update the head table with the fields and values passed as
|
| 379 |
+
keyword arguments.
|
| 380 |
+
"""
|
| 381 |
+
self._updateTableWithValues("head", values)
|
| 382 |
+
|
| 383 |
+
def setupGlyphOrder(self, glyphOrder):
|
| 384 |
+
"""Set the glyph order for the font."""
|
| 385 |
+
self.font.setGlyphOrder(glyphOrder)
|
| 386 |
+
|
| 387 |
+
def setupCharacterMap(self, cmapping, uvs=None, allowFallback=False):
|
| 388 |
+
"""Build the `cmap` table for the font. The `cmapping` argument should
|
| 389 |
+
be a dict mapping unicode code points as integers to glyph names.
|
| 390 |
+
|
| 391 |
+
The `uvs` argument, when passed, must be a list of tuples, describing
|
| 392 |
+
Unicode Variation Sequences. These tuples have three elements:
|
| 393 |
+
(unicodeValue, variationSelector, glyphName)
|
| 394 |
+
`unicodeValue` and `variationSelector` are integer code points.
|
| 395 |
+
`glyphName` may be None, to indicate this is the default variation.
|
| 396 |
+
Text processors will then use the cmap to find the glyph name.
|
| 397 |
+
Each Unicode Variation Sequence should be an officially supported
|
| 398 |
+
sequence, but this is not policed.
|
| 399 |
+
"""
|
| 400 |
+
subTables = []
|
| 401 |
+
highestUnicode = max(cmapping) if cmapping else 0
|
| 402 |
+
if highestUnicode > 0xFFFF:
|
| 403 |
+
cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
|
| 404 |
+
subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
|
| 405 |
+
subTables.append(subTable_3_10)
|
| 406 |
+
else:
|
| 407 |
+
cmapping_3_1 = cmapping
|
| 408 |
+
format = 4
|
| 409 |
+
subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
|
| 410 |
+
try:
|
| 411 |
+
subTable_3_1.compile(self.font)
|
| 412 |
+
except struct.error:
|
| 413 |
+
# format 4 overflowed, fall back to format 12
|
| 414 |
+
if not allowFallback:
|
| 415 |
+
raise ValueError(
|
| 416 |
+
"cmap format 4 subtable overflowed; sort glyph order by unicode to fix."
|
| 417 |
+
)
|
| 418 |
+
format = 12
|
| 419 |
+
subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
|
| 420 |
+
subTables.append(subTable_3_1)
|
| 421 |
+
subTable_0_3 = buildCmapSubTable(cmapping_3_1, format, 0, 3)
|
| 422 |
+
subTables.append(subTable_0_3)
|
| 423 |
+
|
| 424 |
+
if uvs is not None:
|
| 425 |
+
uvsDict = {}
|
| 426 |
+
for unicodeValue, variationSelector, glyphName in uvs:
|
| 427 |
+
if cmapping.get(unicodeValue) == glyphName:
|
| 428 |
+
# this is a default variation
|
| 429 |
+
glyphName = None
|
| 430 |
+
if variationSelector not in uvsDict:
|
| 431 |
+
uvsDict[variationSelector] = []
|
| 432 |
+
uvsDict[variationSelector].append((unicodeValue, glyphName))
|
| 433 |
+
uvsSubTable = buildCmapSubTable({}, 14, 0, 5)
|
| 434 |
+
uvsSubTable.uvsDict = uvsDict
|
| 435 |
+
subTables.append(uvsSubTable)
|
| 436 |
+
|
| 437 |
+
self.font["cmap"] = newTable("cmap")
|
| 438 |
+
self.font["cmap"].tableVersion = 0
|
| 439 |
+
self.font["cmap"].tables = subTables
|
| 440 |
+
|
| 441 |
+
def setupNameTable(self, nameStrings, windows=True, mac=True):
|
| 442 |
+
"""Create the `name` table for the font. The `nameStrings` argument must
|
| 443 |
+
be a dict, mapping nameIDs or descriptive names for the nameIDs to name
|
| 444 |
+
record values. A value is either a string, or a dict, mapping language codes
|
| 445 |
+
to strings, to allow localized name table entries.
|
| 446 |
+
|
| 447 |
+
By default, both Windows (platformID=3) and Macintosh (platformID=1) name
|
| 448 |
+
records are added, unless any of `windows` or `mac` arguments is False.
|
| 449 |
+
|
| 450 |
+
The following descriptive names are available for nameIDs:
|
| 451 |
+
|
| 452 |
+
copyright (nameID 0)
|
| 453 |
+
familyName (nameID 1)
|
| 454 |
+
styleName (nameID 2)
|
| 455 |
+
uniqueFontIdentifier (nameID 3)
|
| 456 |
+
fullName (nameID 4)
|
| 457 |
+
version (nameID 5)
|
| 458 |
+
psName (nameID 6)
|
| 459 |
+
trademark (nameID 7)
|
| 460 |
+
manufacturer (nameID 8)
|
| 461 |
+
designer (nameID 9)
|
| 462 |
+
description (nameID 10)
|
| 463 |
+
vendorURL (nameID 11)
|
| 464 |
+
designerURL (nameID 12)
|
| 465 |
+
licenseDescription (nameID 13)
|
| 466 |
+
licenseInfoURL (nameID 14)
|
| 467 |
+
typographicFamily (nameID 16)
|
| 468 |
+
typographicSubfamily (nameID 17)
|
| 469 |
+
compatibleFullName (nameID 18)
|
| 470 |
+
sampleText (nameID 19)
|
| 471 |
+
postScriptCIDFindfontName (nameID 20)
|
| 472 |
+
wwsFamilyName (nameID 21)
|
| 473 |
+
wwsSubfamilyName (nameID 22)
|
| 474 |
+
lightBackgroundPalette (nameID 23)
|
| 475 |
+
darkBackgroundPalette (nameID 24)
|
| 476 |
+
variationsPostScriptNamePrefix (nameID 25)
|
| 477 |
+
"""
|
| 478 |
+
nameTable = self.font["name"] = newTable("name")
|
| 479 |
+
nameTable.names = []
|
| 480 |
+
|
| 481 |
+
for nameName, nameValue in nameStrings.items():
|
| 482 |
+
if isinstance(nameName, int):
|
| 483 |
+
nameID = nameName
|
| 484 |
+
else:
|
| 485 |
+
nameID = _nameIDs[nameName]
|
| 486 |
+
if isinstance(nameValue, str):
|
| 487 |
+
nameValue = dict(en=nameValue)
|
| 488 |
+
nameTable.addMultilingualName(
|
| 489 |
+
nameValue, ttFont=self.font, nameID=nameID, windows=windows, mac=mac
|
| 490 |
+
)
|
| 491 |
+
|
| 492 |
+
def setupOS2(self, **values):
|
| 493 |
+
"""Create a new `OS/2` table and initialize it with default values,
|
| 494 |
+
which can be overridden by keyword arguments.
|
| 495 |
+
"""
|
| 496 |
+
self._initTableWithValues("OS/2", _getOS2Defaults(), values)
|
| 497 |
+
if "xAvgCharWidth" not in values:
|
| 498 |
+
assert (
|
| 499 |
+
"hmtx" in self.font
|
| 500 |
+
), "the 'hmtx' table must be setup before the 'OS/2' table"
|
| 501 |
+
self.font["OS/2"].recalcAvgCharWidth(self.font)
|
| 502 |
+
if not (
|
| 503 |
+
"ulUnicodeRange1" in values
|
| 504 |
+
or "ulUnicodeRange2" in values
|
| 505 |
+
or "ulUnicodeRange3" in values
|
| 506 |
+
or "ulUnicodeRange3" in values
|
| 507 |
+
):
|
| 508 |
+
assert (
|
| 509 |
+
"cmap" in self.font
|
| 510 |
+
), "the 'cmap' table must be setup before the 'OS/2' table"
|
| 511 |
+
self.font["OS/2"].recalcUnicodeRanges(self.font)
|
| 512 |
+
|
| 513 |
+
def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
|
| 514 |
+
from .cffLib import (
|
| 515 |
+
CFFFontSet,
|
| 516 |
+
TopDictIndex,
|
| 517 |
+
TopDict,
|
| 518 |
+
CharStrings,
|
| 519 |
+
GlobalSubrsIndex,
|
| 520 |
+
PrivateDict,
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
assert not self.isTTF
|
| 524 |
+
self.font.sfntVersion = "OTTO"
|
| 525 |
+
fontSet = CFFFontSet()
|
| 526 |
+
fontSet.major = 1
|
| 527 |
+
fontSet.minor = 0
|
| 528 |
+
fontSet.otFont = self.font
|
| 529 |
+
fontSet.fontNames = [psName]
|
| 530 |
+
fontSet.topDictIndex = TopDictIndex()
|
| 531 |
+
|
| 532 |
+
globalSubrs = GlobalSubrsIndex()
|
| 533 |
+
fontSet.GlobalSubrs = globalSubrs
|
| 534 |
+
private = PrivateDict()
|
| 535 |
+
for key, value in privateDict.items():
|
| 536 |
+
setattr(private, key, value)
|
| 537 |
+
fdSelect = None
|
| 538 |
+
fdArray = None
|
| 539 |
+
|
| 540 |
+
topDict = TopDict()
|
| 541 |
+
topDict.charset = self.font.getGlyphOrder()
|
| 542 |
+
topDict.Private = private
|
| 543 |
+
topDict.GlobalSubrs = fontSet.GlobalSubrs
|
| 544 |
+
for key, value in fontInfo.items():
|
| 545 |
+
setattr(topDict, key, value)
|
| 546 |
+
if "FontMatrix" not in fontInfo:
|
| 547 |
+
scale = 1 / self.font["head"].unitsPerEm
|
| 548 |
+
topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
|
| 549 |
+
|
| 550 |
+
charStrings = CharStrings(
|
| 551 |
+
None, topDict.charset, globalSubrs, private, fdSelect, fdArray
|
| 552 |
+
)
|
| 553 |
+
for glyphName, charString in charStringsDict.items():
|
| 554 |
+
charString.private = private
|
| 555 |
+
charString.globalSubrs = globalSubrs
|
| 556 |
+
charStrings[glyphName] = charString
|
| 557 |
+
topDict.CharStrings = charStrings
|
| 558 |
+
|
| 559 |
+
fontSet.topDictIndex.append(topDict)
|
| 560 |
+
|
| 561 |
+
self.font["CFF "] = newTable("CFF ")
|
| 562 |
+
self.font["CFF "].cff = fontSet
|
| 563 |
+
|
| 564 |
+
def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
|
| 565 |
+
from .cffLib import (
|
| 566 |
+
CFFFontSet,
|
| 567 |
+
TopDictIndex,
|
| 568 |
+
TopDict,
|
| 569 |
+
CharStrings,
|
| 570 |
+
GlobalSubrsIndex,
|
| 571 |
+
PrivateDict,
|
| 572 |
+
FDArrayIndex,
|
| 573 |
+
FontDict,
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
assert not self.isTTF
|
| 577 |
+
self.font.sfntVersion = "OTTO"
|
| 578 |
+
fontSet = CFFFontSet()
|
| 579 |
+
fontSet.major = 2
|
| 580 |
+
fontSet.minor = 0
|
| 581 |
+
|
| 582 |
+
cff2GetGlyphOrder = self.font.getGlyphOrder
|
| 583 |
+
fontSet.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
|
| 584 |
+
|
| 585 |
+
globalSubrs = GlobalSubrsIndex()
|
| 586 |
+
fontSet.GlobalSubrs = globalSubrs
|
| 587 |
+
|
| 588 |
+
if fdArrayList is None:
|
| 589 |
+
fdArrayList = [{}]
|
| 590 |
+
fdSelect = None
|
| 591 |
+
fdArray = FDArrayIndex()
|
| 592 |
+
fdArray.strings = None
|
| 593 |
+
fdArray.GlobalSubrs = globalSubrs
|
| 594 |
+
for privateDict in fdArrayList:
|
| 595 |
+
fontDict = FontDict()
|
| 596 |
+
fontDict.setCFF2(True)
|
| 597 |
+
private = PrivateDict()
|
| 598 |
+
for key, value in privateDict.items():
|
| 599 |
+
setattr(private, key, value)
|
| 600 |
+
fontDict.Private = private
|
| 601 |
+
fdArray.append(fontDict)
|
| 602 |
+
|
| 603 |
+
topDict = TopDict()
|
| 604 |
+
topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
|
| 605 |
+
topDict.FDArray = fdArray
|
| 606 |
+
scale = 1 / self.font["head"].unitsPerEm
|
| 607 |
+
topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
|
| 608 |
+
|
| 609 |
+
private = fdArray[0].Private
|
| 610 |
+
charStrings = CharStrings(None, None, globalSubrs, private, fdSelect, fdArray)
|
| 611 |
+
for glyphName, charString in charStringsDict.items():
|
| 612 |
+
charString.private = private
|
| 613 |
+
charString.globalSubrs = globalSubrs
|
| 614 |
+
charStrings[glyphName] = charString
|
| 615 |
+
topDict.CharStrings = charStrings
|
| 616 |
+
|
| 617 |
+
fontSet.topDictIndex.append(topDict)
|
| 618 |
+
|
| 619 |
+
self.font["CFF2"] = newTable("CFF2")
|
| 620 |
+
self.font["CFF2"].cff = fontSet
|
| 621 |
+
|
| 622 |
+
if regions:
|
| 623 |
+
self.setupCFF2Regions(regions)
|
| 624 |
+
|
| 625 |
+
def setupCFF2Regions(self, regions):
|
| 626 |
+
from .varLib.builder import buildVarRegionList, buildVarData, buildVarStore
|
| 627 |
+
from .cffLib import VarStoreData
|
| 628 |
+
|
| 629 |
+
assert "fvar" in self.font, "fvar must to be set up first"
|
| 630 |
+
assert "CFF2" in self.font, "CFF2 must to be set up first"
|
| 631 |
+
axisTags = [a.axisTag for a in self.font["fvar"].axes]
|
| 632 |
+
varRegionList = buildVarRegionList(regions, axisTags)
|
| 633 |
+
varData = buildVarData(list(range(len(regions))), None, optimize=False)
|
| 634 |
+
varStore = buildVarStore(varRegionList, [varData])
|
| 635 |
+
vstore = VarStoreData(otVarStore=varStore)
|
| 636 |
+
topDict = self.font["CFF2"].cff.topDictIndex[0]
|
| 637 |
+
topDict.VarStore = vstore
|
| 638 |
+
for fontDict in topDict.FDArray:
|
| 639 |
+
fontDict.Private.vstore = vstore
|
| 640 |
+
|
| 641 |
+
def setupGlyf(self, glyphs, calcGlyphBounds=True, validateGlyphFormat=True):
|
| 642 |
+
"""Create the `glyf` table from a dict, that maps glyph names
|
| 643 |
+
to `fontTools.ttLib.tables._g_l_y_f.Glyph` objects, for example
|
| 644 |
+
as made by `fontTools.pens.ttGlyphPen.TTGlyphPen`.
|
| 645 |
+
|
| 646 |
+
If `calcGlyphBounds` is True, the bounds of all glyphs will be
|
| 647 |
+
calculated. Only pass False if your glyph objects already have
|
| 648 |
+
their bounding box values set.
|
| 649 |
+
|
| 650 |
+
If `validateGlyphFormat` is True, raise ValueError if any of the glyphs contains
|
| 651 |
+
cubic curves or is a variable composite but head.glyphDataFormat=0.
|
| 652 |
+
Set it to False to skip the check if you know in advance all the glyphs are
|
| 653 |
+
compatible with the specified glyphDataFormat.
|
| 654 |
+
"""
|
| 655 |
+
assert self.isTTF
|
| 656 |
+
|
| 657 |
+
if validateGlyphFormat and self.font["head"].glyphDataFormat == 0:
|
| 658 |
+
for name, g in glyphs.items():
|
| 659 |
+
if g.numberOfContours > 0 and any(f & flagCubic for f in g.flags):
|
| 660 |
+
raise ValueError(
|
| 661 |
+
f"Glyph {name!r} has cubic Bezier outlines, but glyphDataFormat=0; "
|
| 662 |
+
"either convert to quadratics with cu2qu or set glyphDataFormat=1."
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
self.font["loca"] = newTable("loca")
|
| 666 |
+
self.font["glyf"] = newTable("glyf")
|
| 667 |
+
self.font["glyf"].glyphs = glyphs
|
| 668 |
+
if hasattr(self.font, "glyphOrder"):
|
| 669 |
+
self.font["glyf"].glyphOrder = self.font.glyphOrder
|
| 670 |
+
if calcGlyphBounds:
|
| 671 |
+
self.calcGlyphBounds()
|
| 672 |
+
|
| 673 |
+
def setupFvar(self, axes, instances):
|
| 674 |
+
"""Adds an font variations table to the font.
|
| 675 |
+
|
| 676 |
+
Args:
|
| 677 |
+
axes (list): See below.
|
| 678 |
+
instances (list): See below.
|
| 679 |
+
|
| 680 |
+
``axes`` should be a list of axes, with each axis either supplied as
|
| 681 |
+
a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
|
| 682 |
+
format ```tupletag, minValue, defaultValue, maxValue, name``.
|
| 683 |
+
The ``name`` is either a string, or a dict, mapping language codes
|
| 684 |
+
to strings, to allow localized name table entries.
|
| 685 |
+
|
| 686 |
+
```instances`` should be a list of instances, with each instance either
|
| 687 |
+
supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
|
| 688 |
+
dict with keys ``location`` (mapping of axis tags to float values),
|
| 689 |
+
``stylename`` and (optionally) ``postscriptfontname``.
|
| 690 |
+
The ``stylename`` is either a string, or a dict, mapping language codes
|
| 691 |
+
to strings, to allow localized name table entries.
|
| 692 |
+
"""
|
| 693 |
+
|
| 694 |
+
addFvar(self.font, axes, instances)
|
| 695 |
+
|
| 696 |
+
def setupAvar(self, axes, mappings=None):
|
| 697 |
+
"""Adds an axis variations table to the font.
|
| 698 |
+
|
| 699 |
+
Args:
|
| 700 |
+
axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
|
| 701 |
+
"""
|
| 702 |
+
from .varLib import _add_avar
|
| 703 |
+
|
| 704 |
+
if "fvar" not in self.font:
|
| 705 |
+
raise KeyError("'fvar' table is missing; can't add 'avar'.")
|
| 706 |
+
|
| 707 |
+
axisTags = [axis.axisTag for axis in self.font["fvar"].axes]
|
| 708 |
+
axes = OrderedDict(enumerate(axes)) # Only values are used
|
| 709 |
+
_add_avar(self.font, axes, mappings, axisTags)
|
| 710 |
+
|
| 711 |
+
def setupGvar(self, variations):
|
| 712 |
+
gvar = self.font["gvar"] = newTable("gvar")
|
| 713 |
+
gvar.version = 1
|
| 714 |
+
gvar.reserved = 0
|
| 715 |
+
gvar.variations = variations
|
| 716 |
+
|
| 717 |
+
def setupGVAR(self, variations):
|
| 718 |
+
gvar = self.font["GVAR"] = newTable("GVAR")
|
| 719 |
+
gvar.version = 1
|
| 720 |
+
gvar.reserved = 0
|
| 721 |
+
gvar.variations = variations
|
| 722 |
+
|
| 723 |
+
def calcGlyphBounds(self):
|
| 724 |
+
"""Calculate the bounding boxes of all glyphs in the `glyf` table.
|
| 725 |
+
This is usually not called explicitly by client code.
|
| 726 |
+
"""
|
| 727 |
+
glyphTable = self.font["glyf"]
|
| 728 |
+
for glyph in glyphTable.glyphs.values():
|
| 729 |
+
glyph.recalcBounds(glyphTable)
|
| 730 |
+
|
| 731 |
+
def setupHorizontalMetrics(self, metrics):
|
| 732 |
+
"""Create a new `hmtx` table, for horizontal metrics.
|
| 733 |
+
|
| 734 |
+
The `metrics` argument must be a dict, mapping glyph names to
|
| 735 |
+
`(width, leftSidebearing)` tuples.
|
| 736 |
+
"""
|
| 737 |
+
self.setupMetrics("hmtx", metrics)
|
| 738 |
+
|
| 739 |
+
def setupVerticalMetrics(self, metrics):
|
| 740 |
+
"""Create a new `vmtx` table, for horizontal metrics.
|
| 741 |
+
|
| 742 |
+
The `metrics` argument must be a dict, mapping glyph names to
|
| 743 |
+
`(height, topSidebearing)` tuples.
|
| 744 |
+
"""
|
| 745 |
+
self.setupMetrics("vmtx", metrics)
|
| 746 |
+
|
| 747 |
+
def setupMetrics(self, tableTag, metrics):
|
| 748 |
+
"""See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
|
| 749 |
+
assert tableTag in ("hmtx", "vmtx")
|
| 750 |
+
mtxTable = self.font[tableTag] = newTable(tableTag)
|
| 751 |
+
roundedMetrics = {}
|
| 752 |
+
for gn in metrics:
|
| 753 |
+
w, lsb = metrics[gn]
|
| 754 |
+
roundedMetrics[gn] = int(round(w)), int(round(lsb))
|
| 755 |
+
mtxTable.metrics = roundedMetrics
|
| 756 |
+
|
| 757 |
+
def setupHorizontalHeader(self, **values):
|
| 758 |
+
"""Create a new `hhea` table initialize it with default values,
|
| 759 |
+
which can be overridden by keyword arguments.
|
| 760 |
+
"""
|
| 761 |
+
self._initTableWithValues("hhea", _hheaDefaults, values)
|
| 762 |
+
|
| 763 |
+
def setupVerticalHeader(self, **values):
|
| 764 |
+
"""Create a new `vhea` table initialize it with default values,
|
| 765 |
+
which can be overridden by keyword arguments.
|
| 766 |
+
"""
|
| 767 |
+
self._initTableWithValues("vhea", _vheaDefaults, values)
|
| 768 |
+
|
| 769 |
+
def setupVerticalOrigins(self, verticalOrigins, defaultVerticalOrigin=None):
|
| 770 |
+
"""Create a new `VORG` table. The `verticalOrigins` argument must be
|
| 771 |
+
a dict, mapping glyph names to vertical origin values.
|
| 772 |
+
|
| 773 |
+
The `defaultVerticalOrigin` argument should be the most common vertical
|
| 774 |
+
origin value. If omitted, this value will be derived from the actual
|
| 775 |
+
values in the `verticalOrigins` argument.
|
| 776 |
+
"""
|
| 777 |
+
if defaultVerticalOrigin is None:
|
| 778 |
+
# find the most frequent vorg value
|
| 779 |
+
bag = {}
|
| 780 |
+
for gn in verticalOrigins:
|
| 781 |
+
vorg = verticalOrigins[gn]
|
| 782 |
+
if vorg not in bag:
|
| 783 |
+
bag[vorg] = 1
|
| 784 |
+
else:
|
| 785 |
+
bag[vorg] += 1
|
| 786 |
+
defaultVerticalOrigin = sorted(
|
| 787 |
+
bag, key=lambda vorg: bag[vorg], reverse=True
|
| 788 |
+
)[0]
|
| 789 |
+
self._initTableWithValues(
|
| 790 |
+
"VORG",
|
| 791 |
+
{},
|
| 792 |
+
dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
|
| 793 |
+
)
|
| 794 |
+
vorgTable = self.font["VORG"]
|
| 795 |
+
vorgTable.majorVersion = 1
|
| 796 |
+
vorgTable.minorVersion = 0
|
| 797 |
+
for gn in verticalOrigins:
|
| 798 |
+
vorgTable[gn] = verticalOrigins[gn]
|
| 799 |
+
|
| 800 |
+
def setupPost(self, keepGlyphNames=True, **values):
|
| 801 |
+
"""Create a new `post` table and initialize it with default values,
|
| 802 |
+
which can be overridden by keyword arguments.
|
| 803 |
+
"""
|
| 804 |
+
isCFF2 = "CFF2" in self.font
|
| 805 |
+
postTable = self._initTableWithValues("post", _postDefaults, values)
|
| 806 |
+
if (self.isTTF or isCFF2) and keepGlyphNames:
|
| 807 |
+
postTable.formatType = 2.0
|
| 808 |
+
postTable.extraNames = []
|
| 809 |
+
postTable.mapping = {}
|
| 810 |
+
else:
|
| 811 |
+
postTable.formatType = 3.0
|
| 812 |
+
|
| 813 |
+
def setupMaxp(self):
|
| 814 |
+
"""Create a new `maxp` table. This is called implicitly by FontBuilder
|
| 815 |
+
itself and is usually not called by client code.
|
| 816 |
+
"""
|
| 817 |
+
if self.isTTF:
|
| 818 |
+
defaults = _maxpDefaultsTTF
|
| 819 |
+
else:
|
| 820 |
+
defaults = _maxpDefaultsOTF
|
| 821 |
+
self._initTableWithValues("maxp", defaults, {})
|
| 822 |
+
|
| 823 |
+
def setupDummyDSIG(self):
|
| 824 |
+
"""This adds an empty DSIG table to the font to make some MS applications
|
| 825 |
+
happy. This does not properly sign the font.
|
| 826 |
+
"""
|
| 827 |
+
values = dict(
|
| 828 |
+
ulVersion=1,
|
| 829 |
+
usFlag=0,
|
| 830 |
+
usNumSigs=0,
|
| 831 |
+
signatureRecords=[],
|
| 832 |
+
)
|
| 833 |
+
self._initTableWithValues("DSIG", {}, values)
|
| 834 |
+
|
| 835 |
+
def addOpenTypeFeatures(self, features, filename=None, tables=None, debug=False):
|
| 836 |
+
"""Add OpenType features to the font from a string containing
|
| 837 |
+
Feature File syntax.
|
| 838 |
+
|
| 839 |
+
The `filename` argument is used in error messages and to determine
|
| 840 |
+
where to look for "include" files.
|
| 841 |
+
|
| 842 |
+
The optional `tables` argument can be a list of OTL tables tags to
|
| 843 |
+
build, allowing the caller to only build selected OTL tables. See
|
| 844 |
+
`fontTools.feaLib` for details.
|
| 845 |
+
|
| 846 |
+
The optional `debug` argument controls whether to add source debugging
|
| 847 |
+
information to the font in the `Debg` table.
|
| 848 |
+
"""
|
| 849 |
+
from .feaLib.builder import addOpenTypeFeaturesFromString
|
| 850 |
+
|
| 851 |
+
addOpenTypeFeaturesFromString(
|
| 852 |
+
self.font, features, filename=filename, tables=tables, debug=debug
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
|
| 856 |
+
"""Add conditional substitutions to a Variable Font.
|
| 857 |
+
|
| 858 |
+
See `fontTools.varLib.featureVars.addFeatureVariations`.
|
| 859 |
+
"""
|
| 860 |
+
from .varLib import featureVars
|
| 861 |
+
|
| 862 |
+
if "fvar" not in self.font:
|
| 863 |
+
raise KeyError("'fvar' table is missing; can't add FeatureVariations.")
|
| 864 |
+
|
| 865 |
+
featureVars.addFeatureVariations(
|
| 866 |
+
self.font, conditionalSubstitutions, featureTag=featureTag
|
| 867 |
+
)
|
| 868 |
+
|
| 869 |
+
def setupCOLR(
|
| 870 |
+
self,
|
| 871 |
+
colorLayers,
|
| 872 |
+
version=None,
|
| 873 |
+
varStore=None,
|
| 874 |
+
varIndexMap=None,
|
| 875 |
+
clipBoxes=None,
|
| 876 |
+
allowLayerReuse=True,
|
| 877 |
+
):
|
| 878 |
+
"""Build new COLR table using color layers dictionary.
|
| 879 |
+
|
| 880 |
+
Cf. `fontTools.colorLib.builder.buildCOLR`.
|
| 881 |
+
"""
|
| 882 |
+
from fontTools.colorLib.builder import buildCOLR
|
| 883 |
+
|
| 884 |
+
glyphMap = self.font.getReverseGlyphMap()
|
| 885 |
+
self.font["COLR"] = buildCOLR(
|
| 886 |
+
colorLayers,
|
| 887 |
+
version=version,
|
| 888 |
+
glyphMap=glyphMap,
|
| 889 |
+
varStore=varStore,
|
| 890 |
+
varIndexMap=varIndexMap,
|
| 891 |
+
clipBoxes=clipBoxes,
|
| 892 |
+
allowLayerReuse=allowLayerReuse,
|
| 893 |
+
)
|
| 894 |
+
|
| 895 |
+
def setupCPAL(
|
| 896 |
+
self,
|
| 897 |
+
palettes,
|
| 898 |
+
paletteTypes=None,
|
| 899 |
+
paletteLabels=None,
|
| 900 |
+
paletteEntryLabels=None,
|
| 901 |
+
):
|
| 902 |
+
"""Build new CPAL table using list of palettes.
|
| 903 |
+
|
| 904 |
+
Optionally build CPAL v1 table using paletteTypes, paletteLabels and
|
| 905 |
+
paletteEntryLabels.
|
| 906 |
+
|
| 907 |
+
Cf. `fontTools.colorLib.builder.buildCPAL`.
|
| 908 |
+
"""
|
| 909 |
+
from fontTools.colorLib.builder import buildCPAL
|
| 910 |
+
|
| 911 |
+
self.font["CPAL"] = buildCPAL(
|
| 912 |
+
palettes,
|
| 913 |
+
paletteTypes=paletteTypes,
|
| 914 |
+
paletteLabels=paletteLabels,
|
| 915 |
+
paletteEntryLabels=paletteEntryLabels,
|
| 916 |
+
nameTable=self.font.get("name"),
|
| 917 |
+
)
|
| 918 |
+
|
| 919 |
+
def setupStat(self, axes, locations=None, elidedFallbackName=2):
|
| 920 |
+
"""Build a new 'STAT' table.
|
| 921 |
+
|
| 922 |
+
See `fontTools.otlLib.builder.buildStatTable` for details about
|
| 923 |
+
the arguments.
|
| 924 |
+
"""
|
| 925 |
+
from .otlLib.builder import buildStatTable
|
| 926 |
+
|
| 927 |
+
assert "name" in self.font, "name must to be set up first"
|
| 928 |
+
|
| 929 |
+
buildStatTable(
|
| 930 |
+
self.font,
|
| 931 |
+
axes,
|
| 932 |
+
locations,
|
| 933 |
+
elidedFallbackName,
|
| 934 |
+
macNames=any(nr.platformID == 1 for nr in self.font["name"].names),
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
def buildCmapSubTable(cmapping, format, platformID, platEncID):
|
| 939 |
+
subTable = cmap_classes[format](format)
|
| 940 |
+
subTable.cmap = cmapping
|
| 941 |
+
subTable.platformID = platformID
|
| 942 |
+
subTable.platEncID = platEncID
|
| 943 |
+
subTable.language = 0
|
| 944 |
+
return subTable
|
| 945 |
+
|
| 946 |
+
|
| 947 |
+
def addFvar(font, axes, instances):
|
| 948 |
+
from .ttLib.tables._f_v_a_r import Axis, NamedInstance
|
| 949 |
+
|
| 950 |
+
assert axes
|
| 951 |
+
|
| 952 |
+
fvar = newTable("fvar")
|
| 953 |
+
nameTable = font["name"]
|
| 954 |
+
|
| 955 |
+
# if there are not currently any mac names don't add them here, that's inconsistent
|
| 956 |
+
# https://github.com/fonttools/fonttools/issues/683
|
| 957 |
+
macNames = any(nr.platformID == 1 for nr in getattr(nameTable, "names", ()))
|
| 958 |
+
|
| 959 |
+
# we have all the best ways to express mac names
|
| 960 |
+
platforms = ((3, 1, 0x409),)
|
| 961 |
+
if macNames:
|
| 962 |
+
platforms = ((1, 0, 0),) + platforms
|
| 963 |
+
|
| 964 |
+
for axis_def in axes:
|
| 965 |
+
axis = Axis()
|
| 966 |
+
|
| 967 |
+
if isinstance(axis_def, tuple):
|
| 968 |
+
(
|
| 969 |
+
axis.axisTag,
|
| 970 |
+
axis.minValue,
|
| 971 |
+
axis.defaultValue,
|
| 972 |
+
axis.maxValue,
|
| 973 |
+
name,
|
| 974 |
+
) = axis_def
|
| 975 |
+
else:
|
| 976 |
+
(axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
|
| 977 |
+
axis_def.tag,
|
| 978 |
+
axis_def.minimum,
|
| 979 |
+
axis_def.default,
|
| 980 |
+
axis_def.maximum,
|
| 981 |
+
axis_def.name,
|
| 982 |
+
)
|
| 983 |
+
if axis_def.hidden:
|
| 984 |
+
axis.flags = 0x0001 # HIDDEN_AXIS
|
| 985 |
+
|
| 986 |
+
if isinstance(name, str):
|
| 987 |
+
name = dict(en=name)
|
| 988 |
+
|
| 989 |
+
axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font, mac=macNames)
|
| 990 |
+
fvar.axes.append(axis)
|
| 991 |
+
|
| 992 |
+
for instance in instances:
|
| 993 |
+
if isinstance(instance, dict):
|
| 994 |
+
coordinates = instance["location"]
|
| 995 |
+
name = instance["stylename"]
|
| 996 |
+
psname = instance.get("postscriptfontname")
|
| 997 |
+
else:
|
| 998 |
+
coordinates = instance.location
|
| 999 |
+
name = instance.localisedStyleName or instance.styleName
|
| 1000 |
+
psname = instance.postScriptFontName
|
| 1001 |
+
|
| 1002 |
+
if isinstance(name, str):
|
| 1003 |
+
name = dict(en=name)
|
| 1004 |
+
|
| 1005 |
+
inst = NamedInstance()
|
| 1006 |
+
inst.subfamilyNameID = nameTable.addMultilingualName(
|
| 1007 |
+
name, ttFont=font, mac=macNames
|
| 1008 |
+
)
|
| 1009 |
+
if psname is not None:
|
| 1010 |
+
inst.postscriptNameID = nameTable.addName(psname, platforms=platforms)
|
| 1011 |
+
inst.coordinates = coordinates
|
| 1012 |
+
fvar.instances.append(inst)
|
| 1013 |
+
|
| 1014 |
+
font["fvar"] = fvar
|
lib/python3.12/site-packages/fontTools/help.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pkgutil
|
| 2 |
+
import sys
|
| 3 |
+
import fontTools
|
| 4 |
+
import importlib
|
| 5 |
+
import os
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def main():
|
| 10 |
+
"""Show this help"""
|
| 11 |
+
path = fontTools.__path__
|
| 12 |
+
descriptions = {}
|
| 13 |
+
for pkg in sorted(
|
| 14 |
+
mod.name
|
| 15 |
+
for mod in pkgutil.walk_packages([fontTools.__path__[0]], prefix="fontTools.")
|
| 16 |
+
):
|
| 17 |
+
try:
|
| 18 |
+
imports = __import__(pkg, globals(), locals(), ["main"])
|
| 19 |
+
except ImportError as e:
|
| 20 |
+
continue
|
| 21 |
+
try:
|
| 22 |
+
description = imports.main.__doc__
|
| 23 |
+
# Cython modules seem to return "main()" as the docstring
|
| 24 |
+
if description and description != "main()":
|
| 25 |
+
pkg = pkg.replace("fontTools.", "").replace(".__main__", "")
|
| 26 |
+
# show the docstring's first line only
|
| 27 |
+
descriptions[pkg] = description.splitlines()[0]
|
| 28 |
+
except AttributeError as e:
|
| 29 |
+
pass
|
| 30 |
+
for pkg, description in descriptions.items():
|
| 31 |
+
print("fonttools %-25s %s" % (pkg, description), file=sys.stderr)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
if __name__ == "__main__":
|
| 35 |
+
print("fonttools v%s\n" % fontTools.__version__, file=sys.stderr)
|
| 36 |
+
main()
|
lib/python3.12/site-packages/fontTools/mtiLib/__init__.py
ADDED
|
@@ -0,0 +1,1400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FontDame-to-FontTools for OpenType Layout tables
|
| 2 |
+
#
|
| 3 |
+
# Source language spec is available at:
|
| 4 |
+
# http://monotype.github.io/OpenType_Table_Source/otl_source.html
|
| 5 |
+
# https://github.com/Monotype/OpenType_Table_Source/
|
| 6 |
+
|
| 7 |
+
from fontTools import ttLib
|
| 8 |
+
from fontTools.ttLib.tables._c_m_a_p import cmap_classes
|
| 9 |
+
from fontTools.ttLib.tables import otTables as ot
|
| 10 |
+
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
|
| 11 |
+
from fontTools.otlLib import builder as otl
|
| 12 |
+
from contextlib import contextmanager
|
| 13 |
+
from fontTools.ttLib import newTable
|
| 14 |
+
from fontTools.feaLib.lookupDebugInfo import LOOKUP_DEBUG_ENV_VAR, LOOKUP_DEBUG_INFO_KEY
|
| 15 |
+
from operator import setitem
|
| 16 |
+
import os
|
| 17 |
+
import logging
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class MtiLibError(Exception):
|
| 21 |
+
pass
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ReferenceNotFoundError(MtiLibError):
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class FeatureNotFoundError(ReferenceNotFoundError):
|
| 29 |
+
pass
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class LookupNotFoundError(ReferenceNotFoundError):
|
| 33 |
+
pass
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
log = logging.getLogger("fontTools.mtiLib")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def makeGlyph(s):
|
| 40 |
+
if s[:2] in ["U ", "u "]:
|
| 41 |
+
return ttLib.TTFont._makeGlyphName(int(s[2:], 16))
|
| 42 |
+
elif s[:2] == "# ":
|
| 43 |
+
return "glyph%.5d" % int(s[2:])
|
| 44 |
+
assert s.find(" ") < 0, "Space found in glyph name: %s" % s
|
| 45 |
+
assert s, "Glyph name is empty"
|
| 46 |
+
return s
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def makeGlyphs(l):
|
| 50 |
+
return [makeGlyph(g) for g in l]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def mapLookup(sym, mapping):
|
| 54 |
+
# Lookups are addressed by name. So resolved them using a map if available.
|
| 55 |
+
# Fallback to parsing as lookup index if a map isn't provided.
|
| 56 |
+
if mapping is not None:
|
| 57 |
+
try:
|
| 58 |
+
idx = mapping[sym]
|
| 59 |
+
except KeyError:
|
| 60 |
+
raise LookupNotFoundError(sym)
|
| 61 |
+
else:
|
| 62 |
+
idx = int(sym)
|
| 63 |
+
return idx
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def mapFeature(sym, mapping):
|
| 67 |
+
# Features are referenced by index according the spec. So, if symbol is an
|
| 68 |
+
# integer, use it directly. Otherwise look up in the map if provided.
|
| 69 |
+
try:
|
| 70 |
+
idx = int(sym)
|
| 71 |
+
except ValueError:
|
| 72 |
+
try:
|
| 73 |
+
idx = mapping[sym]
|
| 74 |
+
except KeyError:
|
| 75 |
+
raise FeatureNotFoundError(sym)
|
| 76 |
+
return idx
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def setReference(mapper, mapping, sym, setter, collection, key):
|
| 80 |
+
try:
|
| 81 |
+
mapped = mapper(sym, mapping)
|
| 82 |
+
except ReferenceNotFoundError as e:
|
| 83 |
+
try:
|
| 84 |
+
if mapping is not None:
|
| 85 |
+
mapping.addDeferredMapping(
|
| 86 |
+
lambda ref: setter(collection, key, ref), sym, e
|
| 87 |
+
)
|
| 88 |
+
return
|
| 89 |
+
except AttributeError:
|
| 90 |
+
pass
|
| 91 |
+
raise
|
| 92 |
+
setter(collection, key, mapped)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class DeferredMapping(dict):
|
| 96 |
+
def __init__(self):
|
| 97 |
+
self._deferredMappings = []
|
| 98 |
+
|
| 99 |
+
def addDeferredMapping(self, setter, sym, e):
|
| 100 |
+
log.debug("Adding deferred mapping for symbol '%s' %s", sym, type(e).__name__)
|
| 101 |
+
self._deferredMappings.append((setter, sym, e))
|
| 102 |
+
|
| 103 |
+
def applyDeferredMappings(self):
|
| 104 |
+
for setter, sym, e in self._deferredMappings:
|
| 105 |
+
log.debug(
|
| 106 |
+
"Applying deferred mapping for symbol '%s' %s", sym, type(e).__name__
|
| 107 |
+
)
|
| 108 |
+
try:
|
| 109 |
+
mapped = self[sym]
|
| 110 |
+
except KeyError:
|
| 111 |
+
raise e
|
| 112 |
+
setter(mapped)
|
| 113 |
+
log.debug("Set to %s", mapped)
|
| 114 |
+
self._deferredMappings = []
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def parseScriptList(lines, featureMap=None):
|
| 118 |
+
self = ot.ScriptList()
|
| 119 |
+
records = []
|
| 120 |
+
with lines.between("script table"):
|
| 121 |
+
for line in lines:
|
| 122 |
+
while len(line) < 4:
|
| 123 |
+
line.append("")
|
| 124 |
+
scriptTag, langSysTag, defaultFeature, features = line
|
| 125 |
+
log.debug("Adding script %s language-system %s", scriptTag, langSysTag)
|
| 126 |
+
|
| 127 |
+
langSys = ot.LangSys()
|
| 128 |
+
langSys.LookupOrder = None
|
| 129 |
+
if defaultFeature:
|
| 130 |
+
setReference(
|
| 131 |
+
mapFeature,
|
| 132 |
+
featureMap,
|
| 133 |
+
defaultFeature,
|
| 134 |
+
setattr,
|
| 135 |
+
langSys,
|
| 136 |
+
"ReqFeatureIndex",
|
| 137 |
+
)
|
| 138 |
+
else:
|
| 139 |
+
langSys.ReqFeatureIndex = 0xFFFF
|
| 140 |
+
syms = stripSplitComma(features)
|
| 141 |
+
langSys.FeatureIndex = theList = [3] * len(syms)
|
| 142 |
+
for i, sym in enumerate(syms):
|
| 143 |
+
setReference(mapFeature, featureMap, sym, setitem, theList, i)
|
| 144 |
+
langSys.FeatureCount = len(langSys.FeatureIndex)
|
| 145 |
+
|
| 146 |
+
script = [s for s in records if s.ScriptTag == scriptTag]
|
| 147 |
+
if script:
|
| 148 |
+
script = script[0].Script
|
| 149 |
+
else:
|
| 150 |
+
scriptRec = ot.ScriptRecord()
|
| 151 |
+
scriptRec.ScriptTag = scriptTag + " " * (4 - len(scriptTag))
|
| 152 |
+
scriptRec.Script = ot.Script()
|
| 153 |
+
records.append(scriptRec)
|
| 154 |
+
script = scriptRec.Script
|
| 155 |
+
script.DefaultLangSys = None
|
| 156 |
+
script.LangSysRecord = []
|
| 157 |
+
script.LangSysCount = 0
|
| 158 |
+
|
| 159 |
+
if langSysTag == "default":
|
| 160 |
+
script.DefaultLangSys = langSys
|
| 161 |
+
else:
|
| 162 |
+
langSysRec = ot.LangSysRecord()
|
| 163 |
+
langSysRec.LangSysTag = langSysTag + " " * (4 - len(langSysTag))
|
| 164 |
+
langSysRec.LangSys = langSys
|
| 165 |
+
script.LangSysRecord.append(langSysRec)
|
| 166 |
+
script.LangSysCount = len(script.LangSysRecord)
|
| 167 |
+
|
| 168 |
+
for script in records:
|
| 169 |
+
script.Script.LangSysRecord = sorted(
|
| 170 |
+
script.Script.LangSysRecord, key=lambda rec: rec.LangSysTag
|
| 171 |
+
)
|
| 172 |
+
self.ScriptRecord = sorted(records, key=lambda rec: rec.ScriptTag)
|
| 173 |
+
self.ScriptCount = len(self.ScriptRecord)
|
| 174 |
+
return self
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def parseFeatureList(lines, lookupMap=None, featureMap=None):
|
| 178 |
+
self = ot.FeatureList()
|
| 179 |
+
self.FeatureRecord = []
|
| 180 |
+
with lines.between("feature table"):
|
| 181 |
+
for line in lines:
|
| 182 |
+
name, featureTag, lookups = line
|
| 183 |
+
if featureMap is not None:
|
| 184 |
+
assert name not in featureMap, "Duplicate feature name: %s" % name
|
| 185 |
+
featureMap[name] = len(self.FeatureRecord)
|
| 186 |
+
# If feature name is integer, make sure it matches its index.
|
| 187 |
+
try:
|
| 188 |
+
assert int(name) == len(self.FeatureRecord), "%d %d" % (
|
| 189 |
+
name,
|
| 190 |
+
len(self.FeatureRecord),
|
| 191 |
+
)
|
| 192 |
+
except ValueError:
|
| 193 |
+
pass
|
| 194 |
+
featureRec = ot.FeatureRecord()
|
| 195 |
+
featureRec.FeatureTag = featureTag
|
| 196 |
+
featureRec.Feature = ot.Feature()
|
| 197 |
+
self.FeatureRecord.append(featureRec)
|
| 198 |
+
feature = featureRec.Feature
|
| 199 |
+
feature.FeatureParams = None
|
| 200 |
+
syms = stripSplitComma(lookups)
|
| 201 |
+
feature.LookupListIndex = theList = [None] * len(syms)
|
| 202 |
+
for i, sym in enumerate(syms):
|
| 203 |
+
setReference(mapLookup, lookupMap, sym, setitem, theList, i)
|
| 204 |
+
feature.LookupCount = len(feature.LookupListIndex)
|
| 205 |
+
|
| 206 |
+
self.FeatureCount = len(self.FeatureRecord)
|
| 207 |
+
return self
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def parseLookupFlags(lines):
|
| 211 |
+
flags = 0
|
| 212 |
+
filterset = None
|
| 213 |
+
allFlags = [
|
| 214 |
+
"righttoleft",
|
| 215 |
+
"ignorebaseglyphs",
|
| 216 |
+
"ignoreligatures",
|
| 217 |
+
"ignoremarks",
|
| 218 |
+
"markattachmenttype",
|
| 219 |
+
"markfiltertype",
|
| 220 |
+
]
|
| 221 |
+
while lines.peeks()[0].lower() in allFlags:
|
| 222 |
+
line = next(lines)
|
| 223 |
+
flag = {
|
| 224 |
+
"righttoleft": 0x0001,
|
| 225 |
+
"ignorebaseglyphs": 0x0002,
|
| 226 |
+
"ignoreligatures": 0x0004,
|
| 227 |
+
"ignoremarks": 0x0008,
|
| 228 |
+
}.get(line[0].lower())
|
| 229 |
+
if flag:
|
| 230 |
+
assert line[1].lower() in ["yes", "no"], line[1]
|
| 231 |
+
if line[1].lower() == "yes":
|
| 232 |
+
flags |= flag
|
| 233 |
+
continue
|
| 234 |
+
if line[0].lower() == "markattachmenttype":
|
| 235 |
+
flags |= int(line[1]) << 8
|
| 236 |
+
continue
|
| 237 |
+
if line[0].lower() == "markfiltertype":
|
| 238 |
+
flags |= 0x10
|
| 239 |
+
filterset = int(line[1])
|
| 240 |
+
return flags, filterset
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def parseSingleSubst(lines, font, _lookupMap=None):
|
| 244 |
+
mapping = {}
|
| 245 |
+
for line in lines:
|
| 246 |
+
assert len(line) == 2, line
|
| 247 |
+
line = makeGlyphs(line)
|
| 248 |
+
mapping[line[0]] = line[1]
|
| 249 |
+
return otl.buildSingleSubstSubtable(mapping)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def parseMultiple(lines, font, _lookupMap=None):
|
| 253 |
+
mapping = {}
|
| 254 |
+
for line in lines:
|
| 255 |
+
line = makeGlyphs(line)
|
| 256 |
+
mapping[line[0]] = line[1:]
|
| 257 |
+
return otl.buildMultipleSubstSubtable(mapping)
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def parseAlternate(lines, font, _lookupMap=None):
|
| 261 |
+
mapping = {}
|
| 262 |
+
for line in lines:
|
| 263 |
+
line = makeGlyphs(line)
|
| 264 |
+
mapping[line[0]] = line[1:]
|
| 265 |
+
return otl.buildAlternateSubstSubtable(mapping)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def parseLigature(lines, font, _lookupMap=None):
|
| 269 |
+
mapping = {}
|
| 270 |
+
for line in lines:
|
| 271 |
+
assert len(line) >= 2, line
|
| 272 |
+
line = makeGlyphs(line)
|
| 273 |
+
mapping[tuple(line[1:])] = line[0]
|
| 274 |
+
return otl.buildLigatureSubstSubtable(mapping)
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
def parseSinglePos(lines, font, _lookupMap=None):
|
| 278 |
+
values = {}
|
| 279 |
+
for line in lines:
|
| 280 |
+
assert len(line) == 3, line
|
| 281 |
+
w = line[0].title().replace(" ", "")
|
| 282 |
+
assert w in valueRecordFormatDict
|
| 283 |
+
g = makeGlyph(line[1])
|
| 284 |
+
v = int(line[2])
|
| 285 |
+
if g not in values:
|
| 286 |
+
values[g] = ValueRecord()
|
| 287 |
+
assert not hasattr(values[g], w), (g, w)
|
| 288 |
+
setattr(values[g], w, v)
|
| 289 |
+
return otl.buildSinglePosSubtable(values, font.getReverseGlyphMap())
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def parsePair(lines, font, _lookupMap=None):
|
| 293 |
+
self = ot.PairPos()
|
| 294 |
+
self.ValueFormat1 = self.ValueFormat2 = 0
|
| 295 |
+
typ = lines.peeks()[0].split()[0].lower()
|
| 296 |
+
if typ in ("left", "right"):
|
| 297 |
+
self.Format = 1
|
| 298 |
+
values = {}
|
| 299 |
+
for line in lines:
|
| 300 |
+
assert len(line) == 4, line
|
| 301 |
+
side = line[0].split()[0].lower()
|
| 302 |
+
assert side in ("left", "right"), side
|
| 303 |
+
what = line[0][len(side) :].title().replace(" ", "")
|
| 304 |
+
mask = valueRecordFormatDict[what][0]
|
| 305 |
+
glyph1, glyph2 = makeGlyphs(line[1:3])
|
| 306 |
+
value = int(line[3])
|
| 307 |
+
if not glyph1 in values:
|
| 308 |
+
values[glyph1] = {}
|
| 309 |
+
if not glyph2 in values[glyph1]:
|
| 310 |
+
values[glyph1][glyph2] = (ValueRecord(), ValueRecord())
|
| 311 |
+
rec2 = values[glyph1][glyph2]
|
| 312 |
+
if side == "left":
|
| 313 |
+
self.ValueFormat1 |= mask
|
| 314 |
+
vr = rec2[0]
|
| 315 |
+
else:
|
| 316 |
+
self.ValueFormat2 |= mask
|
| 317 |
+
vr = rec2[1]
|
| 318 |
+
assert not hasattr(vr, what), (vr, what)
|
| 319 |
+
setattr(vr, what, value)
|
| 320 |
+
self.Coverage = makeCoverage(set(values.keys()), font)
|
| 321 |
+
self.PairSet = []
|
| 322 |
+
for glyph1 in self.Coverage.glyphs:
|
| 323 |
+
values1 = values[glyph1]
|
| 324 |
+
pairset = ot.PairSet()
|
| 325 |
+
records = pairset.PairValueRecord = []
|
| 326 |
+
for glyph2 in sorted(values1.keys(), key=font.getGlyphID):
|
| 327 |
+
values2 = values1[glyph2]
|
| 328 |
+
pair = ot.PairValueRecord()
|
| 329 |
+
pair.SecondGlyph = glyph2
|
| 330 |
+
pair.Value1 = values2[0]
|
| 331 |
+
pair.Value2 = values2[1] if self.ValueFormat2 else None
|
| 332 |
+
records.append(pair)
|
| 333 |
+
pairset.PairValueCount = len(pairset.PairValueRecord)
|
| 334 |
+
self.PairSet.append(pairset)
|
| 335 |
+
self.PairSetCount = len(self.PairSet)
|
| 336 |
+
elif typ.endswith("class"):
|
| 337 |
+
self.Format = 2
|
| 338 |
+
classDefs = [None, None]
|
| 339 |
+
while lines.peeks()[0].endswith("class definition begin"):
|
| 340 |
+
typ = lines.peek()[0][: -len("class definition begin")].lower()
|
| 341 |
+
idx, klass = {
|
| 342 |
+
"first": (0, ot.ClassDef1),
|
| 343 |
+
"second": (1, ot.ClassDef2),
|
| 344 |
+
}[typ]
|
| 345 |
+
assert classDefs[idx] is None
|
| 346 |
+
classDefs[idx] = parseClassDef(lines, font, klass=klass)
|
| 347 |
+
self.ClassDef1, self.ClassDef2 = classDefs
|
| 348 |
+
self.Class1Count, self.Class2Count = (
|
| 349 |
+
1 + max(c.classDefs.values()) for c in classDefs
|
| 350 |
+
)
|
| 351 |
+
self.Class1Record = [ot.Class1Record() for i in range(self.Class1Count)]
|
| 352 |
+
for rec1 in self.Class1Record:
|
| 353 |
+
rec1.Class2Record = [ot.Class2Record() for j in range(self.Class2Count)]
|
| 354 |
+
for rec2 in rec1.Class2Record:
|
| 355 |
+
rec2.Value1 = ValueRecord()
|
| 356 |
+
rec2.Value2 = ValueRecord()
|
| 357 |
+
for line in lines:
|
| 358 |
+
assert len(line) == 4, line
|
| 359 |
+
side = line[0].split()[0].lower()
|
| 360 |
+
assert side in ("left", "right"), side
|
| 361 |
+
what = line[0][len(side) :].title().replace(" ", "")
|
| 362 |
+
mask = valueRecordFormatDict[what][0]
|
| 363 |
+
class1, class2, value = (int(x) for x in line[1:4])
|
| 364 |
+
rec2 = self.Class1Record[class1].Class2Record[class2]
|
| 365 |
+
if side == "left":
|
| 366 |
+
self.ValueFormat1 |= mask
|
| 367 |
+
vr = rec2.Value1
|
| 368 |
+
else:
|
| 369 |
+
self.ValueFormat2 |= mask
|
| 370 |
+
vr = rec2.Value2
|
| 371 |
+
assert not hasattr(vr, what), (vr, what)
|
| 372 |
+
setattr(vr, what, value)
|
| 373 |
+
for rec1 in self.Class1Record:
|
| 374 |
+
for rec2 in rec1.Class2Record:
|
| 375 |
+
rec2.Value1 = ValueRecord(self.ValueFormat1, rec2.Value1)
|
| 376 |
+
rec2.Value2 = (
|
| 377 |
+
ValueRecord(self.ValueFormat2, rec2.Value2)
|
| 378 |
+
if self.ValueFormat2
|
| 379 |
+
else None
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
self.Coverage = makeCoverage(set(self.ClassDef1.classDefs.keys()), font)
|
| 383 |
+
else:
|
| 384 |
+
assert 0, typ
|
| 385 |
+
return self
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def parseKernset(lines, font, _lookupMap=None):
|
| 389 |
+
typ = lines.peeks()[0].split()[0].lower()
|
| 390 |
+
if typ in ("left", "right"):
|
| 391 |
+
with lines.until(
|
| 392 |
+
("firstclass definition begin", "secondclass definition begin")
|
| 393 |
+
):
|
| 394 |
+
return parsePair(lines, font)
|
| 395 |
+
return parsePair(lines, font)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def makeAnchor(data, klass=ot.Anchor):
|
| 399 |
+
assert len(data) <= 2
|
| 400 |
+
anchor = klass()
|
| 401 |
+
anchor.Format = 1
|
| 402 |
+
anchor.XCoordinate, anchor.YCoordinate = intSplitComma(data[0])
|
| 403 |
+
if len(data) > 1 and data[1] != "":
|
| 404 |
+
anchor.Format = 2
|
| 405 |
+
anchor.AnchorPoint = int(data[1])
|
| 406 |
+
return anchor
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def parseCursive(lines, font, _lookupMap=None):
|
| 410 |
+
records = {}
|
| 411 |
+
for line in lines:
|
| 412 |
+
assert len(line) in [3, 4], line
|
| 413 |
+
idx, klass = {
|
| 414 |
+
"entry": (0, ot.EntryAnchor),
|
| 415 |
+
"exit": (1, ot.ExitAnchor),
|
| 416 |
+
}[line[0]]
|
| 417 |
+
glyph = makeGlyph(line[1])
|
| 418 |
+
if glyph not in records:
|
| 419 |
+
records[glyph] = [None, None]
|
| 420 |
+
assert records[glyph][idx] is None, (glyph, idx)
|
| 421 |
+
records[glyph][idx] = makeAnchor(line[2:], klass)
|
| 422 |
+
return otl.buildCursivePosSubtable(records, font.getReverseGlyphMap())
|
| 423 |
+
|
| 424 |
+
|
| 425 |
+
def makeMarkRecords(data, coverage, c):
|
| 426 |
+
records = []
|
| 427 |
+
for glyph in coverage.glyphs:
|
| 428 |
+
klass, anchor = data[glyph]
|
| 429 |
+
record = c.MarkRecordClass()
|
| 430 |
+
record.Class = klass
|
| 431 |
+
setattr(record, c.MarkAnchor, anchor)
|
| 432 |
+
records.append(record)
|
| 433 |
+
return records
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def makeBaseRecords(data, coverage, c, classCount):
|
| 437 |
+
records = []
|
| 438 |
+
idx = {}
|
| 439 |
+
for glyph in coverage.glyphs:
|
| 440 |
+
idx[glyph] = len(records)
|
| 441 |
+
record = c.BaseRecordClass()
|
| 442 |
+
anchors = [None] * classCount
|
| 443 |
+
setattr(record, c.BaseAnchor, anchors)
|
| 444 |
+
records.append(record)
|
| 445 |
+
for (glyph, klass), anchor in data.items():
|
| 446 |
+
record = records[idx[glyph]]
|
| 447 |
+
anchors = getattr(record, c.BaseAnchor)
|
| 448 |
+
assert anchors[klass] is None, (glyph, klass)
|
| 449 |
+
anchors[klass] = anchor
|
| 450 |
+
return records
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def makeLigatureRecords(data, coverage, c, classCount):
|
| 454 |
+
records = [None] * len(coverage.glyphs)
|
| 455 |
+
idx = {g: i for i, g in enumerate(coverage.glyphs)}
|
| 456 |
+
|
| 457 |
+
for (glyph, klass, compIdx, compCount), anchor in data.items():
|
| 458 |
+
record = records[idx[glyph]]
|
| 459 |
+
if record is None:
|
| 460 |
+
record = records[idx[glyph]] = ot.LigatureAttach()
|
| 461 |
+
record.ComponentCount = compCount
|
| 462 |
+
record.ComponentRecord = [ot.ComponentRecord() for i in range(compCount)]
|
| 463 |
+
for compRec in record.ComponentRecord:
|
| 464 |
+
compRec.LigatureAnchor = [None] * classCount
|
| 465 |
+
assert record.ComponentCount == compCount, (
|
| 466 |
+
glyph,
|
| 467 |
+
record.ComponentCount,
|
| 468 |
+
compCount,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
anchors = record.ComponentRecord[compIdx - 1].LigatureAnchor
|
| 472 |
+
assert anchors[klass] is None, (glyph, compIdx, klass)
|
| 473 |
+
anchors[klass] = anchor
|
| 474 |
+
return records
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def parseMarkToSomething(lines, font, c):
|
| 478 |
+
self = c.Type()
|
| 479 |
+
self.Format = 1
|
| 480 |
+
markData = {}
|
| 481 |
+
baseData = {}
|
| 482 |
+
Data = {
|
| 483 |
+
"mark": (markData, c.MarkAnchorClass),
|
| 484 |
+
"base": (baseData, c.BaseAnchorClass),
|
| 485 |
+
"ligature": (baseData, c.BaseAnchorClass),
|
| 486 |
+
}
|
| 487 |
+
maxKlass = 0
|
| 488 |
+
for line in lines:
|
| 489 |
+
typ = line[0]
|
| 490 |
+
assert typ in ("mark", "base", "ligature")
|
| 491 |
+
glyph = makeGlyph(line[1])
|
| 492 |
+
data, anchorClass = Data[typ]
|
| 493 |
+
extraItems = 2 if typ == "ligature" else 0
|
| 494 |
+
extras = tuple(int(i) for i in line[2 : 2 + extraItems])
|
| 495 |
+
klass = int(line[2 + extraItems])
|
| 496 |
+
anchor = makeAnchor(line[3 + extraItems :], anchorClass)
|
| 497 |
+
if typ == "mark":
|
| 498 |
+
key, value = glyph, (klass, anchor)
|
| 499 |
+
else:
|
| 500 |
+
key, value = ((glyph, klass) + extras), anchor
|
| 501 |
+
assert key not in data, key
|
| 502 |
+
data[key] = value
|
| 503 |
+
maxKlass = max(maxKlass, klass)
|
| 504 |
+
|
| 505 |
+
# Mark
|
| 506 |
+
markCoverage = makeCoverage(set(markData.keys()), font, c.MarkCoverageClass)
|
| 507 |
+
markArray = c.MarkArrayClass()
|
| 508 |
+
markRecords = makeMarkRecords(markData, markCoverage, c)
|
| 509 |
+
setattr(markArray, c.MarkRecord, markRecords)
|
| 510 |
+
setattr(markArray, c.MarkCount, len(markRecords))
|
| 511 |
+
setattr(self, c.MarkCoverage, markCoverage)
|
| 512 |
+
setattr(self, c.MarkArray, markArray)
|
| 513 |
+
self.ClassCount = maxKlass + 1
|
| 514 |
+
|
| 515 |
+
# Base
|
| 516 |
+
self.classCount = 0 if not baseData else 1 + max(k[1] for k, v in baseData.items())
|
| 517 |
+
baseCoverage = makeCoverage(
|
| 518 |
+
set([k[0] for k in baseData.keys()]), font, c.BaseCoverageClass
|
| 519 |
+
)
|
| 520 |
+
baseArray = c.BaseArrayClass()
|
| 521 |
+
if c.Base == "Ligature":
|
| 522 |
+
baseRecords = makeLigatureRecords(baseData, baseCoverage, c, self.classCount)
|
| 523 |
+
else:
|
| 524 |
+
baseRecords = makeBaseRecords(baseData, baseCoverage, c, self.classCount)
|
| 525 |
+
setattr(baseArray, c.BaseRecord, baseRecords)
|
| 526 |
+
setattr(baseArray, c.BaseCount, len(baseRecords))
|
| 527 |
+
setattr(self, c.BaseCoverage, baseCoverage)
|
| 528 |
+
setattr(self, c.BaseArray, baseArray)
|
| 529 |
+
|
| 530 |
+
return self
|
| 531 |
+
|
| 532 |
+
|
| 533 |
+
class MarkHelper(object):
|
| 534 |
+
def __init__(self):
|
| 535 |
+
for Which in ("Mark", "Base"):
|
| 536 |
+
for What in ("Coverage", "Array", "Count", "Record", "Anchor"):
|
| 537 |
+
key = Which + What
|
| 538 |
+
if Which == "Mark" and What in ("Count", "Record", "Anchor"):
|
| 539 |
+
value = key
|
| 540 |
+
else:
|
| 541 |
+
value = getattr(self, Which) + What
|
| 542 |
+
if value == "LigatureRecord":
|
| 543 |
+
value = "LigatureAttach"
|
| 544 |
+
setattr(self, key, value)
|
| 545 |
+
if What != "Count":
|
| 546 |
+
klass = getattr(ot, value)
|
| 547 |
+
setattr(self, key + "Class", klass)
|
| 548 |
+
|
| 549 |
+
|
| 550 |
+
class MarkToBaseHelper(MarkHelper):
|
| 551 |
+
Mark = "Mark"
|
| 552 |
+
Base = "Base"
|
| 553 |
+
Type = ot.MarkBasePos
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
class MarkToMarkHelper(MarkHelper):
|
| 557 |
+
Mark = "Mark1"
|
| 558 |
+
Base = "Mark2"
|
| 559 |
+
Type = ot.MarkMarkPos
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
class MarkToLigatureHelper(MarkHelper):
|
| 563 |
+
Mark = "Mark"
|
| 564 |
+
Base = "Ligature"
|
| 565 |
+
Type = ot.MarkLigPos
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
def parseMarkToBase(lines, font, _lookupMap=None):
|
| 569 |
+
return parseMarkToSomething(lines, font, MarkToBaseHelper())
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def parseMarkToMark(lines, font, _lookupMap=None):
|
| 573 |
+
return parseMarkToSomething(lines, font, MarkToMarkHelper())
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def parseMarkToLigature(lines, font, _lookupMap=None):
|
| 577 |
+
return parseMarkToSomething(lines, font, MarkToLigatureHelper())
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
def stripSplitComma(line):
|
| 581 |
+
return [s.strip() for s in line.split(",")] if line else []
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
def intSplitComma(line):
|
| 585 |
+
return [int(i) for i in line.split(",")] if line else []
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
# Copied from fontTools.subset
|
| 589 |
+
class ContextHelper(object):
|
| 590 |
+
def __init__(self, klassName, Format):
|
| 591 |
+
if klassName.endswith("Subst"):
|
| 592 |
+
Typ = "Sub"
|
| 593 |
+
Type = "Subst"
|
| 594 |
+
else:
|
| 595 |
+
Typ = "Pos"
|
| 596 |
+
Type = "Pos"
|
| 597 |
+
if klassName.startswith("Chain"):
|
| 598 |
+
Chain = "Chain"
|
| 599 |
+
InputIdx = 1
|
| 600 |
+
DataLen = 3
|
| 601 |
+
else:
|
| 602 |
+
Chain = ""
|
| 603 |
+
InputIdx = 0
|
| 604 |
+
DataLen = 1
|
| 605 |
+
ChainTyp = Chain + Typ
|
| 606 |
+
|
| 607 |
+
self.Typ = Typ
|
| 608 |
+
self.Type = Type
|
| 609 |
+
self.Chain = Chain
|
| 610 |
+
self.ChainTyp = ChainTyp
|
| 611 |
+
self.InputIdx = InputIdx
|
| 612 |
+
self.DataLen = DataLen
|
| 613 |
+
|
| 614 |
+
self.LookupRecord = Type + "LookupRecord"
|
| 615 |
+
|
| 616 |
+
if Format == 1:
|
| 617 |
+
Coverage = lambda r: r.Coverage
|
| 618 |
+
ChainCoverage = lambda r: r.Coverage
|
| 619 |
+
ContextData = lambda r: (None,)
|
| 620 |
+
ChainContextData = lambda r: (None, None, None)
|
| 621 |
+
SetContextData = None
|
| 622 |
+
SetChainContextData = None
|
| 623 |
+
RuleData = lambda r: (r.Input,)
|
| 624 |
+
ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
|
| 625 |
+
|
| 626 |
+
def SetRuleData(r, d):
|
| 627 |
+
(r.Input,) = d
|
| 628 |
+
(r.GlyphCount,) = (len(x) + 1 for x in d)
|
| 629 |
+
|
| 630 |
+
def ChainSetRuleData(r, d):
|
| 631 |
+
(r.Backtrack, r.Input, r.LookAhead) = d
|
| 632 |
+
(
|
| 633 |
+
r.BacktrackGlyphCount,
|
| 634 |
+
r.InputGlyphCount,
|
| 635 |
+
r.LookAheadGlyphCount,
|
| 636 |
+
) = (len(d[0]), len(d[1]) + 1, len(d[2]))
|
| 637 |
+
|
| 638 |
+
elif Format == 2:
|
| 639 |
+
Coverage = lambda r: r.Coverage
|
| 640 |
+
ChainCoverage = lambda r: r.Coverage
|
| 641 |
+
ContextData = lambda r: (r.ClassDef,)
|
| 642 |
+
ChainContextData = lambda r: (
|
| 643 |
+
r.BacktrackClassDef,
|
| 644 |
+
r.InputClassDef,
|
| 645 |
+
r.LookAheadClassDef,
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
def SetContextData(r, d):
|
| 649 |
+
(r.ClassDef,) = d
|
| 650 |
+
|
| 651 |
+
def SetChainContextData(r, d):
|
| 652 |
+
(r.BacktrackClassDef, r.InputClassDef, r.LookAheadClassDef) = d
|
| 653 |
+
|
| 654 |
+
RuleData = lambda r: (r.Class,)
|
| 655 |
+
ChainRuleData = lambda r: (r.Backtrack, r.Input, r.LookAhead)
|
| 656 |
+
|
| 657 |
+
def SetRuleData(r, d):
|
| 658 |
+
(r.Class,) = d
|
| 659 |
+
(r.GlyphCount,) = (len(x) + 1 for x in d)
|
| 660 |
+
|
| 661 |
+
def ChainSetRuleData(r, d):
|
| 662 |
+
(r.Backtrack, r.Input, r.LookAhead) = d
|
| 663 |
+
(
|
| 664 |
+
r.BacktrackGlyphCount,
|
| 665 |
+
r.InputGlyphCount,
|
| 666 |
+
r.LookAheadGlyphCount,
|
| 667 |
+
) = (len(d[0]), len(d[1]) + 1, len(d[2]))
|
| 668 |
+
|
| 669 |
+
elif Format == 3:
|
| 670 |
+
Coverage = lambda r: r.Coverage[0]
|
| 671 |
+
ChainCoverage = lambda r: r.InputCoverage[0]
|
| 672 |
+
ContextData = None
|
| 673 |
+
ChainContextData = None
|
| 674 |
+
SetContextData = None
|
| 675 |
+
SetChainContextData = None
|
| 676 |
+
RuleData = lambda r: r.Coverage
|
| 677 |
+
ChainRuleData = lambda r: (
|
| 678 |
+
r.BacktrackCoverage + r.InputCoverage + r.LookAheadCoverage
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
def SetRuleData(r, d):
|
| 682 |
+
(r.Coverage,) = d
|
| 683 |
+
(r.GlyphCount,) = (len(x) for x in d)
|
| 684 |
+
|
| 685 |
+
def ChainSetRuleData(r, d):
|
| 686 |
+
(r.BacktrackCoverage, r.InputCoverage, r.LookAheadCoverage) = d
|
| 687 |
+
(
|
| 688 |
+
r.BacktrackGlyphCount,
|
| 689 |
+
r.InputGlyphCount,
|
| 690 |
+
r.LookAheadGlyphCount,
|
| 691 |
+
) = (len(x) for x in d)
|
| 692 |
+
|
| 693 |
+
else:
|
| 694 |
+
assert 0, "unknown format: %s" % Format
|
| 695 |
+
|
| 696 |
+
if Chain:
|
| 697 |
+
self.Coverage = ChainCoverage
|
| 698 |
+
self.ContextData = ChainContextData
|
| 699 |
+
self.SetContextData = SetChainContextData
|
| 700 |
+
self.RuleData = ChainRuleData
|
| 701 |
+
self.SetRuleData = ChainSetRuleData
|
| 702 |
+
else:
|
| 703 |
+
self.Coverage = Coverage
|
| 704 |
+
self.ContextData = ContextData
|
| 705 |
+
self.SetContextData = SetContextData
|
| 706 |
+
self.RuleData = RuleData
|
| 707 |
+
self.SetRuleData = SetRuleData
|
| 708 |
+
|
| 709 |
+
if Format == 1:
|
| 710 |
+
self.Rule = ChainTyp + "Rule"
|
| 711 |
+
self.RuleCount = ChainTyp + "RuleCount"
|
| 712 |
+
self.RuleSet = ChainTyp + "RuleSet"
|
| 713 |
+
self.RuleSetCount = ChainTyp + "RuleSetCount"
|
| 714 |
+
self.Intersect = lambda glyphs, c, r: [r] if r in glyphs else []
|
| 715 |
+
elif Format == 2:
|
| 716 |
+
self.Rule = ChainTyp + "ClassRule"
|
| 717 |
+
self.RuleCount = ChainTyp + "ClassRuleCount"
|
| 718 |
+
self.RuleSet = ChainTyp + "ClassSet"
|
| 719 |
+
self.RuleSetCount = ChainTyp + "ClassSetCount"
|
| 720 |
+
self.Intersect = lambda glyphs, c, r: (
|
| 721 |
+
c.intersect_class(glyphs, r)
|
| 722 |
+
if c
|
| 723 |
+
else (set(glyphs) if r == 0 else set())
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
self.ClassDef = "InputClassDef" if Chain else "ClassDef"
|
| 727 |
+
self.ClassDefIndex = 1 if Chain else 0
|
| 728 |
+
self.Input = "Input" if Chain else "Class"
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
def parseLookupRecords(items, klassName, lookupMap=None):
|
| 732 |
+
klass = getattr(ot, klassName)
|
| 733 |
+
lst = []
|
| 734 |
+
for item in items:
|
| 735 |
+
rec = klass()
|
| 736 |
+
item = stripSplitComma(item)
|
| 737 |
+
assert len(item) == 2, item
|
| 738 |
+
idx = int(item[0])
|
| 739 |
+
assert idx > 0, idx
|
| 740 |
+
rec.SequenceIndex = idx - 1
|
| 741 |
+
setReference(mapLookup, lookupMap, item[1], setattr, rec, "LookupListIndex")
|
| 742 |
+
lst.append(rec)
|
| 743 |
+
return lst
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
def makeClassDef(classDefs, font, klass=ot.Coverage):
|
| 747 |
+
if not classDefs:
|
| 748 |
+
return None
|
| 749 |
+
self = klass()
|
| 750 |
+
self.classDefs = dict(classDefs)
|
| 751 |
+
return self
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
def parseClassDef(lines, font, klass=ot.ClassDef):
|
| 755 |
+
classDefs = {}
|
| 756 |
+
with lines.between("class definition"):
|
| 757 |
+
for line in lines:
|
| 758 |
+
glyph = makeGlyph(line[0])
|
| 759 |
+
assert glyph not in classDefs, glyph
|
| 760 |
+
classDefs[glyph] = int(line[1])
|
| 761 |
+
return makeClassDef(classDefs, font, klass)
|
| 762 |
+
|
| 763 |
+
|
| 764 |
+
def makeCoverage(glyphs, font, klass=ot.Coverage):
|
| 765 |
+
if not glyphs:
|
| 766 |
+
return None
|
| 767 |
+
if isinstance(glyphs, set):
|
| 768 |
+
glyphs = sorted(glyphs)
|
| 769 |
+
coverage = klass()
|
| 770 |
+
coverage.glyphs = sorted(set(glyphs), key=font.getGlyphID)
|
| 771 |
+
return coverage
|
| 772 |
+
|
| 773 |
+
|
| 774 |
+
def parseCoverage(lines, font, klass=ot.Coverage):
|
| 775 |
+
glyphs = []
|
| 776 |
+
with lines.between("coverage definition"):
|
| 777 |
+
for line in lines:
|
| 778 |
+
glyphs.append(makeGlyph(line[0]))
|
| 779 |
+
return makeCoverage(glyphs, font, klass)
|
| 780 |
+
|
| 781 |
+
|
| 782 |
+
def bucketizeRules(self, c, rules, bucketKeys):
|
| 783 |
+
buckets = {}
|
| 784 |
+
for seq, recs in rules:
|
| 785 |
+
buckets.setdefault(seq[c.InputIdx][0], []).append(
|
| 786 |
+
(tuple(s[1 if i == c.InputIdx else 0 :] for i, s in enumerate(seq)), recs)
|
| 787 |
+
)
|
| 788 |
+
|
| 789 |
+
rulesets = []
|
| 790 |
+
for firstGlyph in bucketKeys:
|
| 791 |
+
if firstGlyph not in buckets:
|
| 792 |
+
rulesets.append(None)
|
| 793 |
+
continue
|
| 794 |
+
thisRules = []
|
| 795 |
+
for seq, recs in buckets[firstGlyph]:
|
| 796 |
+
rule = getattr(ot, c.Rule)()
|
| 797 |
+
c.SetRuleData(rule, seq)
|
| 798 |
+
setattr(rule, c.Type + "Count", len(recs))
|
| 799 |
+
setattr(rule, c.LookupRecord, recs)
|
| 800 |
+
thisRules.append(rule)
|
| 801 |
+
|
| 802 |
+
ruleset = getattr(ot, c.RuleSet)()
|
| 803 |
+
setattr(ruleset, c.Rule, thisRules)
|
| 804 |
+
setattr(ruleset, c.RuleCount, len(thisRules))
|
| 805 |
+
rulesets.append(ruleset)
|
| 806 |
+
|
| 807 |
+
setattr(self, c.RuleSet, rulesets)
|
| 808 |
+
setattr(self, c.RuleSetCount, len(rulesets))
|
| 809 |
+
|
| 810 |
+
|
| 811 |
+
def parseContext(lines, font, Type, lookupMap=None):
|
| 812 |
+
self = getattr(ot, Type)()
|
| 813 |
+
typ = lines.peeks()[0].split()[0].lower()
|
| 814 |
+
if typ == "glyph":
|
| 815 |
+
self.Format = 1
|
| 816 |
+
log.debug("Parsing %s format %s", Type, self.Format)
|
| 817 |
+
c = ContextHelper(Type, self.Format)
|
| 818 |
+
rules = []
|
| 819 |
+
for line in lines:
|
| 820 |
+
assert line[0].lower() == "glyph", line[0]
|
| 821 |
+
while len(line) < 1 + c.DataLen:
|
| 822 |
+
line.append("")
|
| 823 |
+
seq = tuple(makeGlyphs(stripSplitComma(i)) for i in line[1 : 1 + c.DataLen])
|
| 824 |
+
recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap)
|
| 825 |
+
rules.append((seq, recs))
|
| 826 |
+
|
| 827 |
+
firstGlyphs = set(seq[c.InputIdx][0] for seq, recs in rules)
|
| 828 |
+
self.Coverage = makeCoverage(firstGlyphs, font)
|
| 829 |
+
bucketizeRules(self, c, rules, self.Coverage.glyphs)
|
| 830 |
+
elif typ.endswith("class"):
|
| 831 |
+
self.Format = 2
|
| 832 |
+
log.debug("Parsing %s format %s", Type, self.Format)
|
| 833 |
+
c = ContextHelper(Type, self.Format)
|
| 834 |
+
classDefs = [None] * c.DataLen
|
| 835 |
+
while lines.peeks()[0].endswith("class definition begin"):
|
| 836 |
+
typ = lines.peek()[0][: -len("class definition begin")].lower()
|
| 837 |
+
idx, klass = {
|
| 838 |
+
1: {
|
| 839 |
+
"": (0, ot.ClassDef),
|
| 840 |
+
},
|
| 841 |
+
3: {
|
| 842 |
+
"backtrack": (0, ot.BacktrackClassDef),
|
| 843 |
+
"": (1, ot.InputClassDef),
|
| 844 |
+
"lookahead": (2, ot.LookAheadClassDef),
|
| 845 |
+
},
|
| 846 |
+
}[c.DataLen][typ]
|
| 847 |
+
assert classDefs[idx] is None, idx
|
| 848 |
+
classDefs[idx] = parseClassDef(lines, font, klass=klass)
|
| 849 |
+
c.SetContextData(self, classDefs)
|
| 850 |
+
rules = []
|
| 851 |
+
for line in lines:
|
| 852 |
+
assert line[0].lower().startswith("class"), line[0]
|
| 853 |
+
while len(line) < 1 + c.DataLen:
|
| 854 |
+
line.append("")
|
| 855 |
+
seq = tuple(intSplitComma(i) for i in line[1 : 1 + c.DataLen])
|
| 856 |
+
recs = parseLookupRecords(line[1 + c.DataLen :], c.LookupRecord, lookupMap)
|
| 857 |
+
rules.append((seq, recs))
|
| 858 |
+
firstClasses = set(seq[c.InputIdx][0] for seq, recs in rules)
|
| 859 |
+
firstGlyphs = set(
|
| 860 |
+
g for g, c in classDefs[c.InputIdx].classDefs.items() if c in firstClasses
|
| 861 |
+
)
|
| 862 |
+
self.Coverage = makeCoverage(firstGlyphs, font)
|
| 863 |
+
bucketizeRules(self, c, rules, range(max(firstClasses) + 1))
|
| 864 |
+
elif typ.endswith("coverage"):
|
| 865 |
+
self.Format = 3
|
| 866 |
+
log.debug("Parsing %s format %s", Type, self.Format)
|
| 867 |
+
c = ContextHelper(Type, self.Format)
|
| 868 |
+
coverages = tuple([] for i in range(c.DataLen))
|
| 869 |
+
while lines.peeks()[0].endswith("coverage definition begin"):
|
| 870 |
+
typ = lines.peek()[0][: -len("coverage definition begin")].lower()
|
| 871 |
+
idx, klass = {
|
| 872 |
+
1: {
|
| 873 |
+
"": (0, ot.Coverage),
|
| 874 |
+
},
|
| 875 |
+
3: {
|
| 876 |
+
"backtrack": (0, ot.BacktrackCoverage),
|
| 877 |
+
"input": (1, ot.InputCoverage),
|
| 878 |
+
"lookahead": (2, ot.LookAheadCoverage),
|
| 879 |
+
},
|
| 880 |
+
}[c.DataLen][typ]
|
| 881 |
+
coverages[idx].append(parseCoverage(lines, font, klass=klass))
|
| 882 |
+
c.SetRuleData(self, coverages)
|
| 883 |
+
lines = list(lines)
|
| 884 |
+
assert len(lines) == 1
|
| 885 |
+
line = lines[0]
|
| 886 |
+
assert line[0].lower() == "coverage", line[0]
|
| 887 |
+
recs = parseLookupRecords(line[1:], c.LookupRecord, lookupMap)
|
| 888 |
+
setattr(self, c.Type + "Count", len(recs))
|
| 889 |
+
setattr(self, c.LookupRecord, recs)
|
| 890 |
+
else:
|
| 891 |
+
assert 0, typ
|
| 892 |
+
return self
|
| 893 |
+
|
| 894 |
+
|
| 895 |
+
def parseContextSubst(lines, font, lookupMap=None):
|
| 896 |
+
return parseContext(lines, font, "ContextSubst", lookupMap=lookupMap)
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
def parseContextPos(lines, font, lookupMap=None):
|
| 900 |
+
return parseContext(lines, font, "ContextPos", lookupMap=lookupMap)
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
def parseChainedSubst(lines, font, lookupMap=None):
|
| 904 |
+
return parseContext(lines, font, "ChainContextSubst", lookupMap=lookupMap)
|
| 905 |
+
|
| 906 |
+
|
| 907 |
+
def parseChainedPos(lines, font, lookupMap=None):
|
| 908 |
+
return parseContext(lines, font, "ChainContextPos", lookupMap=lookupMap)
|
| 909 |
+
|
| 910 |
+
|
| 911 |
+
def parseReverseChainedSubst(lines, font, _lookupMap=None):
|
| 912 |
+
self = ot.ReverseChainSingleSubst()
|
| 913 |
+
self.Format = 1
|
| 914 |
+
coverages = ([], [])
|
| 915 |
+
while lines.peeks()[0].endswith("coverage definition begin"):
|
| 916 |
+
typ = lines.peek()[0][: -len("coverage definition begin")].lower()
|
| 917 |
+
idx, klass = {
|
| 918 |
+
"backtrack": (0, ot.BacktrackCoverage),
|
| 919 |
+
"lookahead": (1, ot.LookAheadCoverage),
|
| 920 |
+
}[typ]
|
| 921 |
+
coverages[idx].append(parseCoverage(lines, font, klass=klass))
|
| 922 |
+
self.BacktrackCoverage = coverages[0]
|
| 923 |
+
self.BacktrackGlyphCount = len(self.BacktrackCoverage)
|
| 924 |
+
self.LookAheadCoverage = coverages[1]
|
| 925 |
+
self.LookAheadGlyphCount = len(self.LookAheadCoverage)
|
| 926 |
+
mapping = {}
|
| 927 |
+
for line in lines:
|
| 928 |
+
assert len(line) == 2, line
|
| 929 |
+
line = makeGlyphs(line)
|
| 930 |
+
mapping[line[0]] = line[1]
|
| 931 |
+
self.Coverage = makeCoverage(set(mapping.keys()), font)
|
| 932 |
+
self.Substitute = [mapping[k] for k in self.Coverage.glyphs]
|
| 933 |
+
self.GlyphCount = len(self.Substitute)
|
| 934 |
+
return self
|
| 935 |
+
|
| 936 |
+
|
| 937 |
+
def parseLookup(lines, tableTag, font, lookupMap=None):
|
| 938 |
+
line = lines.expect("lookup")
|
| 939 |
+
_, name, typ = line
|
| 940 |
+
log.debug("Parsing lookup type %s %s", typ, name)
|
| 941 |
+
lookup = ot.Lookup()
|
| 942 |
+
lookup.LookupFlag, filterset = parseLookupFlags(lines)
|
| 943 |
+
if filterset is not None:
|
| 944 |
+
lookup.MarkFilteringSet = filterset
|
| 945 |
+
lookup.LookupType, parseLookupSubTable = {
|
| 946 |
+
"GSUB": {
|
| 947 |
+
"single": (1, parseSingleSubst),
|
| 948 |
+
"multiple": (2, parseMultiple),
|
| 949 |
+
"alternate": (3, parseAlternate),
|
| 950 |
+
"ligature": (4, parseLigature),
|
| 951 |
+
"context": (5, parseContextSubst),
|
| 952 |
+
"chained": (6, parseChainedSubst),
|
| 953 |
+
"reversechained": (8, parseReverseChainedSubst),
|
| 954 |
+
},
|
| 955 |
+
"GPOS": {
|
| 956 |
+
"single": (1, parseSinglePos),
|
| 957 |
+
"pair": (2, parsePair),
|
| 958 |
+
"kernset": (2, parseKernset),
|
| 959 |
+
"cursive": (3, parseCursive),
|
| 960 |
+
"mark to base": (4, parseMarkToBase),
|
| 961 |
+
"mark to ligature": (5, parseMarkToLigature),
|
| 962 |
+
"mark to mark": (6, parseMarkToMark),
|
| 963 |
+
"context": (7, parseContextPos),
|
| 964 |
+
"chained": (8, parseChainedPos),
|
| 965 |
+
},
|
| 966 |
+
}[tableTag][typ]
|
| 967 |
+
|
| 968 |
+
with lines.until("lookup end"):
|
| 969 |
+
subtables = []
|
| 970 |
+
|
| 971 |
+
while lines.peek():
|
| 972 |
+
with lines.until(("% subtable", "subtable end")):
|
| 973 |
+
while lines.peek():
|
| 974 |
+
subtable = parseLookupSubTable(lines, font, lookupMap)
|
| 975 |
+
assert lookup.LookupType == subtable.LookupType
|
| 976 |
+
subtables.append(subtable)
|
| 977 |
+
if lines.peeks()[0] in ("% subtable", "subtable end"):
|
| 978 |
+
next(lines)
|
| 979 |
+
lines.expect("lookup end")
|
| 980 |
+
|
| 981 |
+
lookup.SubTable = subtables
|
| 982 |
+
lookup.SubTableCount = len(lookup.SubTable)
|
| 983 |
+
if lookup.SubTableCount == 0:
|
| 984 |
+
# Remove this return when following is fixed:
|
| 985 |
+
# https://github.com/fonttools/fonttools/issues/789
|
| 986 |
+
return None
|
| 987 |
+
return lookup
|
| 988 |
+
|
| 989 |
+
|
| 990 |
+
def parseGSUBGPOS(lines, font, tableTag):
|
| 991 |
+
container = ttLib.getTableClass(tableTag)()
|
| 992 |
+
lookupMap = DeferredMapping()
|
| 993 |
+
featureMap = DeferredMapping()
|
| 994 |
+
assert tableTag in ("GSUB", "GPOS")
|
| 995 |
+
log.debug("Parsing %s", tableTag)
|
| 996 |
+
self = getattr(ot, tableTag)()
|
| 997 |
+
self.Version = 0x00010000
|
| 998 |
+
fields = {
|
| 999 |
+
"script table begin": (
|
| 1000 |
+
"ScriptList",
|
| 1001 |
+
lambda lines: parseScriptList(lines, featureMap),
|
| 1002 |
+
),
|
| 1003 |
+
"feature table begin": (
|
| 1004 |
+
"FeatureList",
|
| 1005 |
+
lambda lines: parseFeatureList(lines, lookupMap, featureMap),
|
| 1006 |
+
),
|
| 1007 |
+
"lookup": ("LookupList", None),
|
| 1008 |
+
}
|
| 1009 |
+
for attr, parser in fields.values():
|
| 1010 |
+
setattr(self, attr, None)
|
| 1011 |
+
while lines.peek() is not None:
|
| 1012 |
+
typ = lines.peek()[0].lower()
|
| 1013 |
+
if typ not in fields:
|
| 1014 |
+
log.debug("Skipping %s", lines.peek())
|
| 1015 |
+
next(lines)
|
| 1016 |
+
continue
|
| 1017 |
+
attr, parser = fields[typ]
|
| 1018 |
+
if typ == "lookup":
|
| 1019 |
+
if self.LookupList is None:
|
| 1020 |
+
self.LookupList = ot.LookupList()
|
| 1021 |
+
self.LookupList.Lookup = []
|
| 1022 |
+
_, name, _ = lines.peek()
|
| 1023 |
+
lookup = parseLookup(lines, tableTag, font, lookupMap)
|
| 1024 |
+
if lookupMap is not None:
|
| 1025 |
+
assert name not in lookupMap, "Duplicate lookup name: %s" % name
|
| 1026 |
+
lookupMap[name] = len(self.LookupList.Lookup)
|
| 1027 |
+
else:
|
| 1028 |
+
assert int(name) == len(self.LookupList.Lookup), "%d %d" % (
|
| 1029 |
+
name,
|
| 1030 |
+
len(self.Lookup),
|
| 1031 |
+
)
|
| 1032 |
+
self.LookupList.Lookup.append(lookup)
|
| 1033 |
+
else:
|
| 1034 |
+
assert getattr(self, attr) is None, attr
|
| 1035 |
+
setattr(self, attr, parser(lines))
|
| 1036 |
+
if self.LookupList:
|
| 1037 |
+
self.LookupList.LookupCount = len(self.LookupList.Lookup)
|
| 1038 |
+
if lookupMap is not None:
|
| 1039 |
+
lookupMap.applyDeferredMappings()
|
| 1040 |
+
if os.environ.get(LOOKUP_DEBUG_ENV_VAR):
|
| 1041 |
+
if "Debg" not in font:
|
| 1042 |
+
font["Debg"] = newTable("Debg")
|
| 1043 |
+
font["Debg"].data = {}
|
| 1044 |
+
debug = (
|
| 1045 |
+
font["Debg"]
|
| 1046 |
+
.data.setdefault(LOOKUP_DEBUG_INFO_KEY, {})
|
| 1047 |
+
.setdefault(tableTag, {})
|
| 1048 |
+
)
|
| 1049 |
+
for name, lookup in lookupMap.items():
|
| 1050 |
+
debug[str(lookup)] = ["", name, ""]
|
| 1051 |
+
|
| 1052 |
+
featureMap.applyDeferredMappings()
|
| 1053 |
+
container.table = self
|
| 1054 |
+
return container
|
| 1055 |
+
|
| 1056 |
+
|
| 1057 |
+
def parseGSUB(lines, font):
|
| 1058 |
+
return parseGSUBGPOS(lines, font, "GSUB")
|
| 1059 |
+
|
| 1060 |
+
|
| 1061 |
+
def parseGPOS(lines, font):
|
| 1062 |
+
return parseGSUBGPOS(lines, font, "GPOS")
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
+
def parseAttachList(lines, font):
|
| 1066 |
+
points = {}
|
| 1067 |
+
with lines.between("attachment list"):
|
| 1068 |
+
for line in lines:
|
| 1069 |
+
glyph = makeGlyph(line[0])
|
| 1070 |
+
assert glyph not in points, glyph
|
| 1071 |
+
points[glyph] = [int(i) for i in line[1:]]
|
| 1072 |
+
return otl.buildAttachList(points, font.getReverseGlyphMap())
|
| 1073 |
+
|
| 1074 |
+
|
| 1075 |
+
def parseCaretList(lines, font):
|
| 1076 |
+
carets = {}
|
| 1077 |
+
with lines.between("carets"):
|
| 1078 |
+
for line in lines:
|
| 1079 |
+
glyph = makeGlyph(line[0])
|
| 1080 |
+
assert glyph not in carets, glyph
|
| 1081 |
+
num = int(line[1])
|
| 1082 |
+
thisCarets = [int(i) for i in line[2:]]
|
| 1083 |
+
assert num == len(thisCarets), line
|
| 1084 |
+
carets[glyph] = thisCarets
|
| 1085 |
+
return otl.buildLigCaretList(carets, {}, font.getReverseGlyphMap())
|
| 1086 |
+
|
| 1087 |
+
|
| 1088 |
+
def makeMarkFilteringSets(sets, font):
|
| 1089 |
+
self = ot.MarkGlyphSetsDef()
|
| 1090 |
+
self.MarkSetTableFormat = 1
|
| 1091 |
+
self.MarkSetCount = 1 + max(sets.keys())
|
| 1092 |
+
self.Coverage = [None] * self.MarkSetCount
|
| 1093 |
+
for k, v in sorted(sets.items()):
|
| 1094 |
+
self.Coverage[k] = makeCoverage(set(v), font)
|
| 1095 |
+
return self
|
| 1096 |
+
|
| 1097 |
+
|
| 1098 |
+
def parseMarkFilteringSets(lines, font):
|
| 1099 |
+
sets = {}
|
| 1100 |
+
with lines.between("set definition"):
|
| 1101 |
+
for line in lines:
|
| 1102 |
+
assert len(line) == 2, line
|
| 1103 |
+
glyph = makeGlyph(line[0])
|
| 1104 |
+
# TODO accept set names
|
| 1105 |
+
st = int(line[1])
|
| 1106 |
+
if st not in sets:
|
| 1107 |
+
sets[st] = []
|
| 1108 |
+
sets[st].append(glyph)
|
| 1109 |
+
return makeMarkFilteringSets(sets, font)
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
def parseGDEF(lines, font):
|
| 1113 |
+
container = ttLib.getTableClass("GDEF")()
|
| 1114 |
+
log.debug("Parsing GDEF")
|
| 1115 |
+
self = ot.GDEF()
|
| 1116 |
+
fields = {
|
| 1117 |
+
"class definition begin": (
|
| 1118 |
+
"GlyphClassDef",
|
| 1119 |
+
lambda lines, font: parseClassDef(lines, font, klass=ot.GlyphClassDef),
|
| 1120 |
+
),
|
| 1121 |
+
"attachment list begin": ("AttachList", parseAttachList),
|
| 1122 |
+
"carets begin": ("LigCaretList", parseCaretList),
|
| 1123 |
+
"mark attachment class definition begin": (
|
| 1124 |
+
"MarkAttachClassDef",
|
| 1125 |
+
lambda lines, font: parseClassDef(lines, font, klass=ot.MarkAttachClassDef),
|
| 1126 |
+
),
|
| 1127 |
+
"markfilter set definition begin": ("MarkGlyphSetsDef", parseMarkFilteringSets),
|
| 1128 |
+
}
|
| 1129 |
+
for attr, parser in fields.values():
|
| 1130 |
+
setattr(self, attr, None)
|
| 1131 |
+
while lines.peek() is not None:
|
| 1132 |
+
typ = lines.peek()[0].lower()
|
| 1133 |
+
if typ not in fields:
|
| 1134 |
+
log.debug("Skipping %s", typ)
|
| 1135 |
+
next(lines)
|
| 1136 |
+
continue
|
| 1137 |
+
attr, parser = fields[typ]
|
| 1138 |
+
assert getattr(self, attr) is None, attr
|
| 1139 |
+
setattr(self, attr, parser(lines, font))
|
| 1140 |
+
self.Version = 0x00010000 if self.MarkGlyphSetsDef is None else 0x00010002
|
| 1141 |
+
container.table = self
|
| 1142 |
+
return container
|
| 1143 |
+
|
| 1144 |
+
|
| 1145 |
+
def parseCmap(lines, font):
|
| 1146 |
+
container = ttLib.getTableClass("cmap")()
|
| 1147 |
+
log.debug("Parsing cmap")
|
| 1148 |
+
tables = []
|
| 1149 |
+
while lines.peek() is not None:
|
| 1150 |
+
lines.expect("cmap subtable %d" % len(tables))
|
| 1151 |
+
platId, encId, fmt, lang = [
|
| 1152 |
+
parseCmapId(lines, field)
|
| 1153 |
+
for field in ("platformID", "encodingID", "format", "language")
|
| 1154 |
+
]
|
| 1155 |
+
table = cmap_classes[fmt](fmt)
|
| 1156 |
+
table.platformID = platId
|
| 1157 |
+
table.platEncID = encId
|
| 1158 |
+
table.language = lang
|
| 1159 |
+
table.cmap = {}
|
| 1160 |
+
line = next(lines)
|
| 1161 |
+
while line[0] != "end subtable":
|
| 1162 |
+
table.cmap[int(line[0], 16)] = line[1]
|
| 1163 |
+
line = next(lines)
|
| 1164 |
+
tables.append(table)
|
| 1165 |
+
container.tableVersion = 0
|
| 1166 |
+
container.tables = tables
|
| 1167 |
+
return container
|
| 1168 |
+
|
| 1169 |
+
|
| 1170 |
+
def parseCmapId(lines, field):
|
| 1171 |
+
line = next(lines)
|
| 1172 |
+
assert field == line[0]
|
| 1173 |
+
return int(line[1])
|
| 1174 |
+
|
| 1175 |
+
|
| 1176 |
+
def parseTable(lines, font, tableTag=None):
|
| 1177 |
+
log.debug("Parsing table")
|
| 1178 |
+
line = lines.peeks()
|
| 1179 |
+
tag = None
|
| 1180 |
+
if line[0].split()[0] == "FontDame":
|
| 1181 |
+
tag = line[0].split()[1]
|
| 1182 |
+
elif " ".join(line[0].split()[:3]) == "Font Chef Table":
|
| 1183 |
+
tag = line[0].split()[3]
|
| 1184 |
+
if tag is not None:
|
| 1185 |
+
next(lines)
|
| 1186 |
+
tag = tag.ljust(4)
|
| 1187 |
+
if tableTag is None:
|
| 1188 |
+
tableTag = tag
|
| 1189 |
+
else:
|
| 1190 |
+
assert tableTag == tag, (tableTag, tag)
|
| 1191 |
+
|
| 1192 |
+
assert (
|
| 1193 |
+
tableTag is not None
|
| 1194 |
+
), "Don't know what table to parse and data doesn't specify"
|
| 1195 |
+
|
| 1196 |
+
return {
|
| 1197 |
+
"GSUB": parseGSUB,
|
| 1198 |
+
"GPOS": parseGPOS,
|
| 1199 |
+
"GDEF": parseGDEF,
|
| 1200 |
+
"cmap": parseCmap,
|
| 1201 |
+
}[tableTag](lines, font)
|
| 1202 |
+
|
| 1203 |
+
|
| 1204 |
+
class Tokenizer(object):
|
| 1205 |
+
def __init__(self, f):
|
| 1206 |
+
# TODO BytesIO / StringIO as needed? also, figure out whether we work on bytes or unicode
|
| 1207 |
+
lines = iter(f)
|
| 1208 |
+
try:
|
| 1209 |
+
self.filename = f.name
|
| 1210 |
+
except:
|
| 1211 |
+
self.filename = None
|
| 1212 |
+
self.lines = iter(lines)
|
| 1213 |
+
self.line = ""
|
| 1214 |
+
self.lineno = 0
|
| 1215 |
+
self.stoppers = []
|
| 1216 |
+
self.buffer = None
|
| 1217 |
+
|
| 1218 |
+
def __iter__(self):
|
| 1219 |
+
return self
|
| 1220 |
+
|
| 1221 |
+
def _next_line(self):
|
| 1222 |
+
self.lineno += 1
|
| 1223 |
+
line = self.line = next(self.lines)
|
| 1224 |
+
line = [s.strip() for s in line.split("\t")]
|
| 1225 |
+
if len(line) == 1 and not line[0]:
|
| 1226 |
+
del line[0]
|
| 1227 |
+
if line and not line[-1]:
|
| 1228 |
+
log.warning("trailing tab found on line %d: %s" % (self.lineno, self.line))
|
| 1229 |
+
while line and not line[-1]:
|
| 1230 |
+
del line[-1]
|
| 1231 |
+
return line
|
| 1232 |
+
|
| 1233 |
+
def _next_nonempty(self):
|
| 1234 |
+
while True:
|
| 1235 |
+
line = self._next_line()
|
| 1236 |
+
# Skip comments and empty lines
|
| 1237 |
+
if line and line[0] and (line[0][0] != "%" or line[0] == "% subtable"):
|
| 1238 |
+
return line
|
| 1239 |
+
|
| 1240 |
+
def _next_buffered(self):
|
| 1241 |
+
if self.buffer:
|
| 1242 |
+
ret = self.buffer
|
| 1243 |
+
self.buffer = None
|
| 1244 |
+
return ret
|
| 1245 |
+
else:
|
| 1246 |
+
return self._next_nonempty()
|
| 1247 |
+
|
| 1248 |
+
def __next__(self):
|
| 1249 |
+
line = self._next_buffered()
|
| 1250 |
+
if line[0].lower() in self.stoppers:
|
| 1251 |
+
self.buffer = line
|
| 1252 |
+
raise StopIteration
|
| 1253 |
+
return line
|
| 1254 |
+
|
| 1255 |
+
def next(self):
|
| 1256 |
+
return self.__next__()
|
| 1257 |
+
|
| 1258 |
+
def peek(self):
|
| 1259 |
+
if not self.buffer:
|
| 1260 |
+
try:
|
| 1261 |
+
self.buffer = self._next_nonempty()
|
| 1262 |
+
except StopIteration:
|
| 1263 |
+
return None
|
| 1264 |
+
if self.buffer[0].lower() in self.stoppers:
|
| 1265 |
+
return None
|
| 1266 |
+
return self.buffer
|
| 1267 |
+
|
| 1268 |
+
def peeks(self):
|
| 1269 |
+
ret = self.peek()
|
| 1270 |
+
return ret if ret is not None else ("",)
|
| 1271 |
+
|
| 1272 |
+
@contextmanager
|
| 1273 |
+
def between(self, tag):
|
| 1274 |
+
start = tag + " begin"
|
| 1275 |
+
end = tag + " end"
|
| 1276 |
+
self.expectendswith(start)
|
| 1277 |
+
self.stoppers.append(end)
|
| 1278 |
+
yield
|
| 1279 |
+
del self.stoppers[-1]
|
| 1280 |
+
self.expect(tag + " end")
|
| 1281 |
+
|
| 1282 |
+
@contextmanager
|
| 1283 |
+
def until(self, tags):
|
| 1284 |
+
if type(tags) is not tuple:
|
| 1285 |
+
tags = (tags,)
|
| 1286 |
+
self.stoppers.extend(tags)
|
| 1287 |
+
yield
|
| 1288 |
+
del self.stoppers[-len(tags) :]
|
| 1289 |
+
|
| 1290 |
+
def expect(self, s):
|
| 1291 |
+
line = next(self)
|
| 1292 |
+
tag = line[0].lower()
|
| 1293 |
+
assert tag == s, "Expected '%s', got '%s'" % (s, tag)
|
| 1294 |
+
return line
|
| 1295 |
+
|
| 1296 |
+
def expectendswith(self, s):
|
| 1297 |
+
line = next(self)
|
| 1298 |
+
tag = line[0].lower()
|
| 1299 |
+
assert tag.endswith(s), "Expected '*%s', got '%s'" % (s, tag)
|
| 1300 |
+
return line
|
| 1301 |
+
|
| 1302 |
+
|
| 1303 |
+
def build(f, font, tableTag=None):
|
| 1304 |
+
"""Convert a Monotype font layout file to an OpenType layout object
|
| 1305 |
+
|
| 1306 |
+
A font object must be passed, but this may be a "dummy" font; it is only
|
| 1307 |
+
used for sorting glyph sets when making coverage tables and to hold the
|
| 1308 |
+
OpenType layout table while it is being built.
|
| 1309 |
+
|
| 1310 |
+
Args:
|
| 1311 |
+
f: A file object.
|
| 1312 |
+
font (TTFont): A font object.
|
| 1313 |
+
tableTag (string): If provided, asserts that the file contains data for the
|
| 1314 |
+
given OpenType table.
|
| 1315 |
+
|
| 1316 |
+
Returns:
|
| 1317 |
+
An object representing the table. (e.g. ``table_G_S_U_B_``)
|
| 1318 |
+
"""
|
| 1319 |
+
lines = Tokenizer(f)
|
| 1320 |
+
return parseTable(lines, font, tableTag=tableTag)
|
| 1321 |
+
|
| 1322 |
+
|
| 1323 |
+
def main(args=None, font=None):
|
| 1324 |
+
"""Convert a FontDame OTL file to TTX XML
|
| 1325 |
+
|
| 1326 |
+
Writes XML output to stdout.
|
| 1327 |
+
|
| 1328 |
+
Args:
|
| 1329 |
+
args: Command line arguments (``--font``, ``--table``, input files).
|
| 1330 |
+
"""
|
| 1331 |
+
import sys
|
| 1332 |
+
from fontTools import configLogger
|
| 1333 |
+
from fontTools.misc.testTools import MockFont
|
| 1334 |
+
|
| 1335 |
+
if args is None:
|
| 1336 |
+
args = sys.argv[1:]
|
| 1337 |
+
|
| 1338 |
+
# configure the library logger (for >= WARNING)
|
| 1339 |
+
configLogger()
|
| 1340 |
+
# comment this out to enable debug messages from mtiLib's logger
|
| 1341 |
+
# log.setLevel(logging.DEBUG)
|
| 1342 |
+
|
| 1343 |
+
import argparse
|
| 1344 |
+
|
| 1345 |
+
parser = argparse.ArgumentParser(
|
| 1346 |
+
"fonttools mtiLib",
|
| 1347 |
+
description=main.__doc__,
|
| 1348 |
+
)
|
| 1349 |
+
|
| 1350 |
+
parser.add_argument(
|
| 1351 |
+
"--font",
|
| 1352 |
+
"-f",
|
| 1353 |
+
metavar="FILE",
|
| 1354 |
+
dest="font",
|
| 1355 |
+
help="Input TTF files (used for glyph classes and sorting coverage tables)",
|
| 1356 |
+
)
|
| 1357 |
+
parser.add_argument(
|
| 1358 |
+
"--table",
|
| 1359 |
+
"-t",
|
| 1360 |
+
metavar="TABLE",
|
| 1361 |
+
dest="tableTag",
|
| 1362 |
+
help="Table to fill (sniffed from input file if not provided)",
|
| 1363 |
+
)
|
| 1364 |
+
parser.add_argument(
|
| 1365 |
+
"inputs", metavar="FILE", type=str, nargs="+", help="Input FontDame .txt files"
|
| 1366 |
+
)
|
| 1367 |
+
|
| 1368 |
+
args = parser.parse_args(args)
|
| 1369 |
+
|
| 1370 |
+
if font is None:
|
| 1371 |
+
if args.font:
|
| 1372 |
+
font = ttLib.TTFont(args.font)
|
| 1373 |
+
else:
|
| 1374 |
+
font = MockFont()
|
| 1375 |
+
|
| 1376 |
+
for f in args.inputs:
|
| 1377 |
+
log.debug("Processing %s", f)
|
| 1378 |
+
with open(f, "rt", encoding="utf-8-sig") as f:
|
| 1379 |
+
table = build(f, font, tableTag=args.tableTag)
|
| 1380 |
+
blob = table.compile(font) # Make sure it compiles
|
| 1381 |
+
decompiled = table.__class__()
|
| 1382 |
+
decompiled.decompile(blob, font) # Make sure it decompiles!
|
| 1383 |
+
|
| 1384 |
+
# continue
|
| 1385 |
+
from fontTools.misc import xmlWriter
|
| 1386 |
+
|
| 1387 |
+
tag = table.tableTag
|
| 1388 |
+
writer = xmlWriter.XMLWriter(sys.stdout)
|
| 1389 |
+
writer.begintag(tag)
|
| 1390 |
+
writer.newline()
|
| 1391 |
+
# table.toXML(writer, font)
|
| 1392 |
+
decompiled.toXML(writer, font)
|
| 1393 |
+
writer.endtag(tag)
|
| 1394 |
+
writer.newline()
|
| 1395 |
+
|
| 1396 |
+
|
| 1397 |
+
if __name__ == "__main__":
|
| 1398 |
+
import sys
|
| 1399 |
+
|
| 1400 |
+
sys.exit(main())
|
lib/python3.12/site-packages/fontTools/mtiLib/__main__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from fontTools.mtiLib import main
|
| 3 |
+
|
| 4 |
+
if __name__ == "__main__":
|
| 5 |
+
sys.exit(main())
|
lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (72.3 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/mtiLib/__pycache__/__main__.cpython-312.pyc
ADDED
|
Binary file (372 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/otlLib/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenType Layout-related functionality."""
|
lib/python3.12/site-packages/fontTools/otlLib/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (248 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/otlLib/__pycache__/error.cpython-312.pyc
ADDED
|
Binary file (918 Bytes). View file
|
|
|
lib/python3.12/site-packages/fontTools/otlLib/__pycache__/maxContextCalc.cpython-312.pyc
ADDED
|
Binary file (3.64 kB). View file
|
|
|
lib/python3.12/site-packages/fontTools/otlLib/builder.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
lib/python3.12/site-packages/fontTools/otlLib/error.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class OpenTypeLibError(Exception):
|
| 2 |
+
def __init__(self, message, location):
|
| 3 |
+
Exception.__init__(self, message)
|
| 4 |
+
self.location = location
|
| 5 |
+
|
| 6 |
+
def __str__(self):
|
| 7 |
+
message = Exception.__str__(self)
|
| 8 |
+
if self.location:
|
| 9 |
+
return f"{self.location}: {message}"
|
| 10 |
+
else:
|
| 11 |
+
return message
|
lib/python3.12/site-packages/fontTools/otlLib/maxContextCalc.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__all__ = ["maxCtxFont"]
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def maxCtxFont(font):
|
| 5 |
+
"""Calculate the usMaxContext value for an entire font."""
|
| 6 |
+
|
| 7 |
+
maxCtx = 0
|
| 8 |
+
for tag in ("GSUB", "GPOS"):
|
| 9 |
+
if tag not in font:
|
| 10 |
+
continue
|
| 11 |
+
table = font[tag].table
|
| 12 |
+
if not table.LookupList:
|
| 13 |
+
continue
|
| 14 |
+
for lookup in table.LookupList.Lookup:
|
| 15 |
+
for st in lookup.SubTable:
|
| 16 |
+
maxCtx = maxCtxSubtable(maxCtx, tag, lookup.LookupType, st)
|
| 17 |
+
return maxCtx
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def maxCtxSubtable(maxCtx, tag, lookupType, st):
|
| 21 |
+
"""Calculate usMaxContext based on a single lookup table (and an existing
|
| 22 |
+
max value).
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
# single positioning, single / multiple substitution
|
| 26 |
+
if (tag == "GPOS" and lookupType == 1) or (
|
| 27 |
+
tag == "GSUB" and lookupType in (1, 2, 3)
|
| 28 |
+
):
|
| 29 |
+
maxCtx = max(maxCtx, 1)
|
| 30 |
+
|
| 31 |
+
# pair positioning
|
| 32 |
+
elif tag == "GPOS" and lookupType == 2:
|
| 33 |
+
maxCtx = max(maxCtx, 2)
|
| 34 |
+
|
| 35 |
+
# ligatures
|
| 36 |
+
elif tag == "GSUB" and lookupType == 4:
|
| 37 |
+
for ligatures in st.ligatures.values():
|
| 38 |
+
for ligature in ligatures:
|
| 39 |
+
maxCtx = max(maxCtx, ligature.CompCount)
|
| 40 |
+
|
| 41 |
+
# context
|
| 42 |
+
elif (tag == "GPOS" and lookupType == 7) or (tag == "GSUB" and lookupType == 5):
|
| 43 |
+
maxCtx = maxCtxContextualSubtable(maxCtx, st, "Pos" if tag == "GPOS" else "Sub")
|
| 44 |
+
|
| 45 |
+
# chained context
|
| 46 |
+
elif (tag == "GPOS" and lookupType == 8) or (tag == "GSUB" and lookupType == 6):
|
| 47 |
+
maxCtx = maxCtxContextualSubtable(
|
| 48 |
+
maxCtx, st, "Pos" if tag == "GPOS" else "Sub", "Chain"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# extensions
|
| 52 |
+
elif (tag == "GPOS" and lookupType == 9) or (tag == "GSUB" and lookupType == 7):
|
| 53 |
+
maxCtx = maxCtxSubtable(maxCtx, tag, st.ExtensionLookupType, st.ExtSubTable)
|
| 54 |
+
|
| 55 |
+
# reverse-chained context
|
| 56 |
+
elif tag == "GSUB" and lookupType == 8:
|
| 57 |
+
maxCtx = maxCtxContextualRule(maxCtx, st, "Reverse")
|
| 58 |
+
|
| 59 |
+
return maxCtx
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def maxCtxContextualSubtable(maxCtx, st, ruleType, chain=""):
|
| 63 |
+
"""Calculate usMaxContext based on a contextual feature subtable."""
|
| 64 |
+
|
| 65 |
+
if st.Format == 1:
|
| 66 |
+
for ruleset in getattr(st, "%s%sRuleSet" % (chain, ruleType)):
|
| 67 |
+
if ruleset is None:
|
| 68 |
+
continue
|
| 69 |
+
for rule in getattr(ruleset, "%s%sRule" % (chain, ruleType)):
|
| 70 |
+
if rule is None:
|
| 71 |
+
continue
|
| 72 |
+
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
|
| 73 |
+
|
| 74 |
+
elif st.Format == 2:
|
| 75 |
+
for ruleset in getattr(st, "%s%sClassSet" % (chain, ruleType)):
|
| 76 |
+
if ruleset is None:
|
| 77 |
+
continue
|
| 78 |
+
for rule in getattr(ruleset, "%s%sClassRule" % (chain, ruleType)):
|
| 79 |
+
if rule is None:
|
| 80 |
+
continue
|
| 81 |
+
maxCtx = maxCtxContextualRule(maxCtx, rule, chain)
|
| 82 |
+
|
| 83 |
+
elif st.Format == 3:
|
| 84 |
+
maxCtx = maxCtxContextualRule(maxCtx, st, chain)
|
| 85 |
+
|
| 86 |
+
return maxCtx
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def maxCtxContextualRule(maxCtx, st, chain):
|
| 90 |
+
"""Calculate usMaxContext based on a contextual feature rule."""
|
| 91 |
+
|
| 92 |
+
if not chain:
|
| 93 |
+
return max(maxCtx, st.GlyphCount)
|
| 94 |
+
elif chain == "Reverse":
|
| 95 |
+
return max(maxCtx, 1 + st.LookAheadGlyphCount)
|
| 96 |
+
return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount)
|
lib/python3.12/site-packages/fontTools/otlLib/optimize/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from argparse import RawTextHelpFormatter
|
| 2 |
+
from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL, compact
|
| 3 |
+
from fontTools.ttLib import TTFont
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def main(args=None):
|
| 7 |
+
"""Optimize the layout tables of an existing font"""
|
| 8 |
+
from argparse import ArgumentParser
|
| 9 |
+
|
| 10 |
+
from fontTools import configLogger
|
| 11 |
+
|
| 12 |
+
parser = ArgumentParser(
|
| 13 |
+
prog="otlLib.optimize",
|
| 14 |
+
description=main.__doc__,
|
| 15 |
+
formatter_class=RawTextHelpFormatter,
|
| 16 |
+
)
|
| 17 |
+
parser.add_argument("font")
|
| 18 |
+
parser.add_argument(
|
| 19 |
+
"-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--gpos-compression-level",
|
| 23 |
+
help=COMPRESSION_LEVEL.help,
|
| 24 |
+
default=COMPRESSION_LEVEL.default,
|
| 25 |
+
choices=list(range(10)),
|
| 26 |
+
type=int,
|
| 27 |
+
)
|
| 28 |
+
logging_group = parser.add_mutually_exclusive_group(required=False)
|
| 29 |
+
logging_group.add_argument(
|
| 30 |
+
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
| 31 |
+
)
|
| 32 |
+
logging_group.add_argument(
|
| 33 |
+
"-q", "--quiet", action="store_true", help="Turn verbosity off."
|
| 34 |
+
)
|
| 35 |
+
options = parser.parse_args(args)
|
| 36 |
+
|
| 37 |
+
configLogger(
|
| 38 |
+
level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
font = TTFont(options.font)
|
| 42 |
+
compact(font, options.gpos_compression_level)
|
| 43 |
+
font.save(options.outfile or options.font)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
if __name__ == "__main__":
|
| 47 |
+
import sys
|
| 48 |
+
|
| 49 |
+
if len(sys.argv) > 1:
|
| 50 |
+
sys.exit(main())
|
| 51 |
+
import doctest
|
| 52 |
+
|
| 53 |
+
sys.exit(doctest.testmod().failed)
|
lib/python3.12/site-packages/fontTools/otlLib/optimize/__main__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from fontTools.otlLib.optimize import main
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
sys.exit(main())
|
lib/python3.12/site-packages/fontTools/otlLib/optimize/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (2.54 kB). View file
|
|
|