aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorElliott Hughes <enh@google.com>2022-09-01 01:05:19 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2022-09-01 01:05:19 +0000
commite47f00b7523ba5d28bc3654ef83125d8e7dc0e90 (patch)
treee2eb0b9f93f0a8cb6705199d2f738f35cba28e7f
parent312bed341d812d31e893eb9ed2b4bc0dd4da7fff (diff)
parent14ad209ddd0825427bd5f75052aa55f764b98891 (diff)
downloadfonttools-e47f00b7523ba5d28bc3654ef83125d8e7dc0e90.tar.gz
Upgrade fonttools to 4.37.1 am: ae8de171b8 am: ae308ea7c8 am: 14ad209ddd
Original change: https://android-review.googlesource.com/c/platform/external/fonttools/+/2193410 Change-Id: I6c8759cf0dc486db368dc2e7d569ddb3af37f018 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/publish.yml7
-rw-r--r--.github/workflows/test.yml31
-rw-r--r--Doc/docs-requirements.txt6
-rw-r--r--Doc/source/designspaceLib/xml.rst5
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/cffLib/__init__.py2
-rw-r--r--Lib/fontTools/cffLib/specializer.py37
-rw-r--r--Lib/fontTools/cffLib/width.py4
-rw-r--r--Lib/fontTools/colorLib/builder.py155
-rw-r--r--Lib/fontTools/colorLib/unbuilder.py12
-rw-r--r--Lib/fontTools/designspaceLib/__init__.py43
-rw-r--r--Lib/fontTools/designspaceLib/split.py21
-rw-r--r--Lib/fontTools/designspaceLib/statNames.py37
-rw-r--r--Lib/fontTools/designspaceLib/types.py29
-rw-r--r--Lib/fontTools/feaLib/builder.py4
-rw-r--r--Lib/fontTools/feaLib/parser.py36
-rw-r--r--Lib/fontTools/fontBuilder.py2
-rw-r--r--Lib/fontTools/merge/cmap.py4
-rw-r--r--Lib/fontTools/misc/cliTools.py9
-rw-r--r--Lib/fontTools/misc/symfont.py63
-rw-r--r--Lib/fontTools/misc/treeTools.py45
-rw-r--r--Lib/fontTools/misc/visitor.py143
-rw-r--r--Lib/fontTools/mtiLib/__init__.py4
-rw-r--r--Lib/fontTools/otlLib/optimize/__init__.py2
-rw-r--r--Lib/fontTools/pens/basePen.py3
-rw-r--r--Lib/fontTools/pens/cairoPen.py26
-rw-r--r--Lib/fontTools/pens/momentsPen.py662
-rw-r--r--Lib/fontTools/pens/qtPen.py4
-rw-r--r--Lib/fontTools/pens/statisticsPen.py12
-rw-r--r--Lib/fontTools/pens/svgPathPen.py79
-rw-r--r--Lib/fontTools/subset/__init__.py26
-rw-r--r--Lib/fontTools/subset/cff.py11
-rw-r--r--Lib/fontTools/subset/svg.py17
-rw-r--r--Lib/fontTools/svgLib/path/parser.py9
-rw-r--r--Lib/fontTools/ttLib/scaleUpem.py336
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_D_T_.py7
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_L_C_.py6
-rw-r--r--Lib/fontTools/ttLib/tables/S_V_G_.py62
-rw-r--r--Lib/fontTools/ttLib/tables/_c_m_a_p.py8
-rw-r--r--Lib/fontTools/ttLib/tables/_g_l_y_f.py4
-rw-r--r--Lib/fontTools/ttLib/tables/_g_v_a_r.py63
-rw-r--r--Lib/fontTools/ttLib/tables/_k_e_r_n.py4
-rw-r--r--Lib/fontTools/ttLib/tables/otBase.py214
-rw-r--r--Lib/fontTools/ttLib/tables/otConverters.py112
-rwxr-xr-xLib/fontTools/ttLib/tables/otData.py106
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py60
-rw-r--r--Lib/fontTools/ttLib/tables/otTraverse.py137
-rw-r--r--Lib/fontTools/ttLib/ttFont.py132
-rw-r--r--Lib/fontTools/ttLib/ttGlyphSet.py221
-rw-r--r--Lib/fontTools/ttLib/ttVisitor.py32
-rwxr-xr-xLib/fontTools/ufoLib/__init__.py5
-rwxr-xr-xLib/fontTools/ufoLib/glifLib.py3
-rw-r--r--Lib/fontTools/varLib/__init__.py54
-rw-r--r--Lib/fontTools/varLib/cff.py1
-rw-r--r--Lib/fontTools/varLib/errors.py59
-rw-r--r--Lib/fontTools/varLib/featureVars.py16
-rw-r--r--Lib/fontTools/varLib/instancer/__init__.py154
-rw-r--r--Lib/fontTools/varLib/interpolatable.py71
-rw-r--r--Lib/fontTools/varLib/iup.py240
-rw-r--r--Lib/fontTools/varLib/merger.py457
-rw-r--r--Lib/fontTools/varLib/models.py162
-rw-r--r--Lib/fontTools/varLib/mutator.py5
-rw-r--r--Lib/fontTools/varLib/varStore.py24
-rw-r--r--METADATA8
-rw-r--r--NEWS.rst149
-rw-r--r--Snippets/print-json.py153
-rw-r--r--Tests/colorLib/builder_test.py84
-rw-r--r--Tests/colorLib/unbuilder_test.py24
-rw-r--r--Tests/designspaceLib/data/DS5BreakTest.designspace56
-rw-r--r--Tests/designspaceLib/data/test_v5.designspace74
-rw-r--r--Tests/designspaceLib/designspace_v5_test.py50
-rw-r--r--Tests/designspaceLib/statNames_test.py19
-rw-r--r--Tests/feaLib/data/name.fea12
-rw-r--r--Tests/feaLib/data/name.ttx18
-rw-r--r--Tests/feaLib/parser_test.py2
-rw-r--r--Tests/merge/data/CFFFont_expected.ttx48
-rw-r--r--Tests/misc/treeTools_test.py80
-rw-r--r--Tests/misc/visitor_test.py72
-rw-r--r--Tests/pens/cu2quPen_test.py9
-rw-r--r--Tests/subset/data/expect_no_notdef_outline_cid.ttx2
-rw-r--r--Tests/subset/data/expect_notdef_width_cid.ttx2
-rw-r--r--Tests/subset/subset_test.py63
-rw-r--r--Tests/svgLib/path/parser_test.py11
-rw-r--r--Tests/ttLib/data/I-512upem.ttx3777
-rw-r--r--Tests/ttLib/data/I.ttfbin0 -> 13200 bytes
-rw-r--r--Tests/ttLib/scaleUpem_test.py75
-rw-r--r--Tests/ttLib/tables/C_O_L_R_test.py26
-rw-r--r--Tests/ttLib/tables/S_V_G__test.py37
-rw-r--r--Tests/ttLib/ttGlyphSet_test.py112
-rw-r--r--Tests/ttLib/ttVisitor_test.py39
-rw-r--r--Tests/varLib/data/TestVariableCOLR.designspace18
-rw-r--r--Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx357
-rw-r--r--Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx335
-rw-r--r--Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx220
-rw-r--r--Tests/varLib/instancer/data/STATInstancerTest.ttx1830
-rw-r--r--Tests/varLib/instancer/instancer_test.py34
-rw-r--r--Tests/varLib/iup_test.py53
-rw-r--r--Tests/varLib/merger_test.py1844
-rw-r--r--Tests/varLib/models_test.py332
-rw-r--r--Tests/varLib/stat_test.py12
-rw-r--r--Tests/varLib/varLib_test.py12
-rw-r--r--Tests/varLib/varStore_test.py2
-rw-r--r--requirements.txt15
-rw-r--r--setup.cfg4
-rwxr-xr-xsetup.py8
106 files changed, 13099 insertions, 1292 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..5ace4600
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index ec6abe20..ea5ebc9f 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -9,14 +9,17 @@ on:
tags:
- '*.*.*' # e.g. 1.0.0 or 20.15.10
+permissions:
+ contents: read
+
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fb314ef8..0ce1c2d5 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,15 +6,18 @@ on:
pull_request:
branches: [main]
+permissions:
+ contents: read
+
jobs:
lint:
runs-on: ubuntu-latest
# https://github.community/t/github-actions-does-not-respect-skip-ci/17325/8
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python 3.x
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Install packages
@@ -27,21 +30,17 @@ jobs:
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
strategy:
matrix:
- python-version: [3.7, 3.8, 3.9]
+ python-version: ["3.7", "3.10"]
platform: [ubuntu-latest, macos-latest, windows-latest]
- exclude: # Only test on the oldest and latest supported stable Python on macOS and Windows.
+ exclude: # Only test on the latest supported stable Python on macOS and Windows.
- platform: macos-latest
python-version: 3.7
- - platform: macos-latest
- python-version: 3.9
- platform: windows-latest
python-version: 3.7
- - platform: windows-latest
- python-version: 3.9
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install packages
@@ -55,7 +54,7 @@ jobs:
coverage combine
coverage xml
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v2
+ uses: codecov/codecov-action@v3
with:
file: coverage.xml
flags: unittests
@@ -66,11 +65,11 @@ jobs:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python 3.x
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
- python-version: 3.9
+ python-version: "3.10"
- name: Install packages
run: pip install tox
- name: Run Tox
@@ -80,9 +79,9 @@ jobs:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python pypy3
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: "pypy-3.7"
- name: Install packages
diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt
index 5016e340..59f1cd19 100644
--- a/Doc/docs-requirements.txt
+++ b/Doc/docs-requirements.txt
@@ -1,4 +1,4 @@
-sphinx==4.3.2
+sphinx==5.1.1
sphinx_rtd_theme==1.0.0
-reportlab==3.6.5
-freetype-py==2.2.0
+reportlab==3.6.11
+freetype-py==2.3.0
diff --git a/Doc/source/designspaceLib/xml.rst b/Doc/source/designspaceLib/xml.rst
index 58cf6a77..6267b025 100644
--- a/Doc/source/designspaceLib/xml.rst
+++ b/Doc/source/designspaceLib/xml.rst
@@ -717,7 +717,10 @@ The ``<variable-fonts>`` element contains one or more ``<variable-font>`` elemen
<axis-subset name="Weight" userminimum="400" usermaximum="500" userdefault="400"/>
3. a specific value along that axis; then the axis is not functional in the VF
- but the design space is sliced at the given location.
+ but the design space is sliced at the given location. *Note:* While valid to have a
+ specific value that doesn’t have a matching ``<source>`` at that value, currently there
+ isn’t an implentation that supports this. See `this fontmake issue
+ <https://github.com/googlefonts/fontmake/issues/920>`.
.. code:: xml
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 9a39ea0f..5b2cca1f 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.33.3"
+version = __version__ = "4.37.1"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py
index fc82bb27..3eda9ba4 100644
--- a/Lib/fontTools/cffLib/__init__.py
+++ b/Lib/fontTools/cffLib/__init__.py
@@ -1037,6 +1037,8 @@ class VarStoreData(object):
return len(self.data)
def getNumRegions(self, vsIndex):
+ if vsIndex is None:
+ vsIndex = 0
varData = self.otVarStore.VarData[vsIndex]
numRegions = varData.VarRegionCount
return numRegions
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index fbfefa92..677f03b7 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -304,7 +304,7 @@ def _convertBlendOpToArgs(blendList):
deltaArgs = args[numBlends:]
numDeltaValues = len(deltaArgs)
deltaList = [ deltaArgs[i:i + numRegions] for i in range(0, numDeltaValues, numRegions) ]
- blend_args = [ a + b for a, b in zip(defaultArgs,deltaList)]
+ blend_args = [ a + b + [1] for a, b in zip(defaultArgs,deltaList)]
return blend_args
def generalizeCommands(commands, ignoreErrors=False):
@@ -399,10 +399,10 @@ def _convertToBlendCmds(args):
else:
prev_stack_use = stack_use
# The arg is a tuple of blend values.
- # These are each (master 0,delta 1..delta n)
+ # These are each (master 0,delta 1..delta n, 1)
# Combine as many successive tuples as we can,
# up to the max stack limit.
- num_sources = len(arg)
+ num_sources = len(arg) - 1
blendlist = [arg]
i += 1
stack_use += 1 + num_sources # 1 for the num_blends arg
@@ -427,7 +427,8 @@ def _convertToBlendCmds(args):
for arg in blendlist:
blend_args.append(arg[0])
for arg in blendlist:
- blend_args.extend(arg[1:])
+ assert arg[-1] == 1
+ blend_args.extend(arg[1:-1])
blend_args.append(num_blends)
new_args.append(blend_args)
stack_use = prev_stack_use + num_blends
@@ -437,12 +438,13 @@ def _convertToBlendCmds(args):
def _addArgs(a, b):
if isinstance(b, list):
if isinstance(a, list):
- if len(a) != len(b):
+ if len(a) != len(b) or a[-1] != b[-1]:
raise ValueError()
- return [_addArgs(va, vb) for va,vb in zip(a, b)]
+ return [_addArgs(va, vb) for va,vb in zip(a[:-1], b[:-1])] + [a[-1]]
else:
a, b = b, a
if isinstance(a, list):
+ assert a[-1] == 1
return [_addArgs(a[0], b)] + a[1:]
return a + b
@@ -739,12 +741,27 @@ if __name__ == '__main__':
if len(sys.argv) == 1:
import doctest
sys.exit(doctest.testmod().failed)
- program = stringToProgram(sys.argv[1:])
+
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools cffLib.specialer", description="CFF CharString generalizer/specializer")
+ parser.add_argument(
+ "program", metavar="command", nargs="*", help="Commands.")
+ parser.add_argument(
+ "--num-regions", metavar="NumRegions", nargs="*", default=None,
+ help="Number of variable-font regions for blend opertaions.")
+
+ options = parser.parse_args(sys.argv[1:])
+
+ getNumRegions = None if options.num_regions is None else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex])
+
+ program = stringToProgram(options.program)
print("Program:"); print(programToString(program))
- commands = programToCommands(program)
+ commands = programToCommands(program, getNumRegions)
print("Commands:"); print(commands)
program2 = commandsToProgram(commands)
print("Program from commands:"); print(programToString(program2))
assert program == program2
- print("Generalized program:"); print(programToString(generalizeProgram(program)))
- print("Specialized program:"); print(programToString(specializeProgram(program)))
+ print("Generalized program:"); print(programToString(generalizeProgram(program, getNumRegions)))
+ print("Specialized program:"); print(programToString(specializeProgram(program, getNumRegions)))
diff --git a/Lib/fontTools/cffLib/width.py b/Lib/fontTools/cffLib/width.py
index 00b859bb..303c9462 100644
--- a/Lib/fontTools/cffLib/width.py
+++ b/Lib/fontTools/cffLib/width.py
@@ -135,13 +135,13 @@ def optimizeWidths(widths):
dfltC = nomnCost[nominal] - bestCost[nominal]
ends = []
if dfltC == dfltCostU[nominal]:
- starts = [nominal, nominal-108, nominal-1131]
+ starts = [nominal, nominal-108, nominal-1132]
for start in starts:
while cumMaxU[start] and cumMaxU[start] == cumMaxU[start-1]:
start -= 1
ends.append(start)
else:
- starts = [nominal, nominal+108, nominal+1131]
+ starts = [nominal, nominal+108, nominal+1132]
for start in starts:
while cumMaxD[start] and cumMaxD[start] == cumMaxD[start+1]:
start += 1
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 2577fa76..442bc20e 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -23,6 +23,7 @@ from typing import (
)
from fontTools.misc.arrayTools import intRect
from fontTools.misc.fixedTools import fixedToFloat
+from fontTools.misc.treeTools import build_n_ary_tree
from fontTools.ttLib.tables import C_O_L_R_
from fontTools.ttLib.tables import C_P_A_L_
from fontTools.ttLib.tables import _n_a_m_e
@@ -186,10 +187,12 @@ def populateCOLRv0(
def buildCOLR(
colorGlyphs: _ColorGlyphsDict,
version: Optional[int] = None,
+ *,
glyphMap: Optional[Mapping[str, int]] = None,
varStore: Optional[ot.VarStore] = None,
varIndexMap: Optional[ot.DeltaSetIndexMap] = None,
clipBoxes: Optional[Dict[str, _ClipBoxInput]] = None,
+ allowLayerReuse: bool = True,
) -> C_O_L_R_.table_C_O_L_R_:
"""Build COLR table from color layers mapping.
@@ -231,7 +234,11 @@ def buildCOLR(
populateCOLRv0(colr, colorGlyphsV0, glyphMap)
- colr.LayerList, colr.BaseGlyphList = buildColrV1(colorGlyphsV1, glyphMap)
+ colr.LayerList, colr.BaseGlyphList = buildColrV1(
+ colorGlyphsV1,
+ glyphMap,
+ allowLayerReuse=allowLayerReuse,
+ )
if version is None:
version = 1 if (varStore or colorGlyphsV1) else 0
@@ -242,9 +249,6 @@ def buildCOLR(
if version == 0:
self.ColorLayers = self._decompileColorLayersV0(colr)
else:
- clipBoxes = {
- name: clipBoxes[name] for name in clipBoxes or {} if name in colorGlyphsV1
- }
colr.ClipList = buildClipList(clipBoxes) if clipBoxes else None
colr.VarIndexMap = varIndexMap
colr.VarStore = varStore
@@ -443,29 +447,16 @@ def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
yield (lbound, ubound)
-class LayerListBuilder:
- layers: List[ot.Paint]
+class LayerReuseCache:
reusePool: Mapping[Tuple[Any, ...], int]
tuples: Mapping[int, Tuple[Any, ...]]
keepAlive: List[ot.Paint] # we need id to remain valid
def __init__(self):
- self.layers = []
self.reusePool = {}
self.tuples = {}
self.keepAlive = []
- # We need to intercept construction of PaintColrLayers
- callbacks = _buildPaintCallbacks()
- callbacks[
- (
- BuildCallback.BEFORE_BUILD,
- ot.Paint,
- ot.PaintFormat.PaintColrLayers,
- )
- ] = self._beforeBuildPaintColrLayers
- self.tableBuilder = TableBuilder(callbacks)
-
def _paint_tuple(self, paint: ot.Paint):
# start simple, who even cares about cyclic graphs or interesting field types
def _tuple_safe(value):
@@ -491,25 +482,7 @@ class LayerListBuilder:
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
return tuple(self._paint_tuple(p) for p in paints)
- # COLR layers is unusual in that it modifies shared state
- # so we need a callback into an object
- def _beforeBuildPaintColrLayers(self, dest, source):
- # Sketchy gymnastics: a sequence input will have dropped it's layers
- # into NumLayers; get it back
- if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
- layers = source["NumLayers"]
- else:
- layers = source["Layers"]
-
- # Convert maps seqs or whatever into typed objects
- layers = [self.buildPaint(l) for l in layers]
-
- # No reason to have a colr layers with just one entry
- if len(layers) == 1:
- return layers[0], {}
-
- # Look for reuse, with preference to longer sequences
- # This may make the layer list smaller
+ def try_reuse(self, layers: List[ot.Paint]) -> List[ot.Paint]:
found_reuse = True
while found_reuse:
found_reuse = False
@@ -532,10 +505,63 @@ class LayerListBuilder:
layers = layers[:lbound] + [new_slice] + layers[ubound:]
found_reuse = True
break
+ return layers
+
+ def add(self, layers: List[ot.Paint], first_layer_index: int):
+ for lbound, ubound in _reuse_ranges(len(layers)):
+ self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
+ lbound + first_layer_index
+ )
+
+
+class LayerListBuilder:
+ layers: List[ot.Paint]
+ cache: LayerReuseCache
+ allowLayerReuse: bool
+
+ def __init__(self, *, allowLayerReuse=True):
+ self.layers = []
+ if allowLayerReuse:
+ self.cache = LayerReuseCache()
+ else:
+ self.cache = None
+
+ # We need to intercept construction of PaintColrLayers
+ callbacks = _buildPaintCallbacks()
+ callbacks[
+ (
+ BuildCallback.BEFORE_BUILD,
+ ot.Paint,
+ ot.PaintFormat.PaintColrLayers,
+ )
+ ] = self._beforeBuildPaintColrLayers
+ self.tableBuilder = TableBuilder(callbacks)
+
+ # COLR layers is unusual in that it modifies shared state
+ # so we need a callback into an object
+ def _beforeBuildPaintColrLayers(self, dest, source):
+ # Sketchy gymnastics: a sequence input will have dropped it's layers
+ # into NumLayers; get it back
+ if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
+ layers = source["NumLayers"]
+ else:
+ layers = source["Layers"]
+
+ # Convert maps seqs or whatever into typed objects
+ layers = [self.buildPaint(l) for l in layers]
+
+ # No reason to have a colr layers with just one entry
+ if len(layers) == 1:
+ return layers[0], {}
+
+ if self.cache is not None:
+ # Look for reuse, with preference to longer sequences
+ # This may make the layer list smaller
+ layers = self.cache.try_reuse(layers)
# The layer list is now final; if it's too big we need to tree it
is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
- layers = _build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
+ layers = build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
# We now have a tree of sequences with Paint leaves.
# Convert the sequences into PaintColrLayers.
@@ -563,11 +589,8 @@ class LayerListBuilder:
# Register our parts for reuse provided we aren't a tree
# If we are a tree the leaves registered for reuse and that will suffice
- if not is_tree:
- for lbound, ubound in _reuse_ranges(len(layers)):
- self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
- lbound + paint.FirstLayerIndex
- )
+ if self.cache is not None and not is_tree:
+ self.cache.add(layers, paint.FirstLayerIndex)
# we've fully built dest; empty source prevents generalized build from kicking in
return paint, {}
@@ -603,6 +626,8 @@ def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
def buildColrV1(
colorGlyphs: _ColorGlyphsDict,
glyphMap: Optional[Mapping[str, int]] = None,
+ *,
+ allowLayerReuse: bool = True,
) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
if glyphMap is not None:
colorGlyphItems = sorted(
@@ -613,7 +638,7 @@ def buildColrV1(
errors = {}
baseGlyphs = []
- layerBuilder = LayerListBuilder()
+ layerBuilder = LayerListBuilder(allowLayerReuse=allowLayerReuse)
for baseGlyph, paint in colorGlyphItems:
try:
baseGlyphs.append(buildBaseGlyphPaintRecord(baseGlyph, layerBuilder, paint))
@@ -632,45 +657,3 @@ def buildColrV1(
glyphs.BaseGlyphCount = len(baseGlyphs)
glyphs.BaseGlyphPaintRecord = baseGlyphs
return (layers, glyphs)
-
-
-def _build_n_ary_tree(leaves, n):
- """Build N-ary tree from sequence of leaf nodes.
-
- Return a list of lists where each non-leaf node is a list containing
- max n nodes.
- """
- if not leaves:
- return []
-
- assert n > 1
-
- depth = ceil(log(len(leaves), n))
-
- if depth <= 1:
- return list(leaves)
-
- # Fully populate complete subtrees of root until we have enough leaves left
- root = []
- unassigned = None
- full_step = n ** (depth - 1)
- for i in range(0, len(leaves), full_step):
- subtree = leaves[i : i + full_step]
- if len(subtree) < full_step:
- unassigned = subtree
- break
- while len(subtree) > n:
- subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
- root.append(subtree)
-
- if unassigned:
- # Recurse to fill the last subtree, which is the only partially populated one
- subtree = _build_n_ary_tree(unassigned, n)
- if len(subtree) <= n - len(root):
- # replace last subtree with its children if they can still fit
- root.extend(subtree)
- else:
- root.append(subtree)
- assert len(root) <= n
-
- return root
diff --git a/Lib/fontTools/colorLib/unbuilder.py b/Lib/fontTools/colorLib/unbuilder.py
index 03458907..ac243550 100644
--- a/Lib/fontTools/colorLib/unbuilder.py
+++ b/Lib/fontTools/colorLib/unbuilder.py
@@ -13,12 +13,12 @@ def unbuildColrV1(layerList, baseGlyphList):
}
-def _flatten(lst):
- for el in lst:
- if isinstance(el, list):
- yield from _flatten(el)
+def _flatten_layers(lst):
+ for paint in lst:
+ if paint["Format"] == ot.PaintFormat.PaintColrLayers:
+ yield from _flatten_layers(paint["Layers"])
else:
- yield el
+ yield paint
class LayerListUnbuilder:
@@ -41,7 +41,7 @@ class LayerListUnbuilder:
assert source["Format"] == ot.PaintFormat.PaintColrLayers
layers = list(
- _flatten(
+ _flatten_layers(
[
self.unbuildPaint(childPaint)
for childPaint in self.layers[
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
index 400e960e..c74b5509 100644
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -8,7 +8,7 @@ import os
import posixpath
from io import BytesIO, StringIO
from textwrap import indent
-from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union
+from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union, cast
from fontTools.misc import etree as ET
from fontTools.misc import plistlib
@@ -22,9 +22,20 @@ from fontTools.misc.textTools import tobytes, tostr
"""
__all__ = [
- 'DesignSpaceDocumentError', 'DesignSpaceDocument', 'SourceDescriptor',
- 'InstanceDescriptor', 'AxisDescriptor', 'RuleDescriptor', 'BaseDocReader',
- 'BaseDocWriter'
+ 'AxisDescriptor',
+ 'AxisLabelDescriptor',
+ 'BaseDocReader',
+ 'BaseDocWriter',
+ 'DesignSpaceDocument',
+ 'DesignSpaceDocumentError',
+ 'DiscreteAxisDescriptor',
+ 'InstanceDescriptor',
+ 'LocationLabelDescriptor',
+ 'RangeAxisSubsetDescriptor',
+ 'RuleDescriptor',
+ 'SourceDescriptor',
+ 'ValueAxisSubsetDescriptor',
+ 'VariableFontDescriptor',
]
# ElementTree allows to find namespace-prefixed elements, but not attributes
@@ -950,6 +961,7 @@ class DiscreteAxisDescriptor(AbstractAxisDescriptor):
a2 = DiscreteAxisDescriptor()
a2.values = [0, 1]
+ a2.default = 0
a2.name = "Italic"
a2.tag = "ITAL"
a2.labelNames['fr'] = "Italique"
@@ -1352,7 +1364,7 @@ class BaseDocWriter(object):
minVersion = self.documentObject.formatTuple
if (
any(
- isinstance(axis, DiscreteAxisDescriptor) or
+ hasattr(axis, 'values') or
axis.axisOrdering is not None or
axis.axisLabels
for axis in self.documentObject.axes
@@ -1445,10 +1457,10 @@ class BaseDocWriter(object):
for label in axisObject.axisLabels:
self._addAxisLabel(labelsElement, label)
axisElement.append(labelsElement)
- if isinstance(axisObject, AxisDescriptor):
+ if hasattr(axisObject, "minimum"):
axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum)
axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum)
- elif isinstance(axisObject, DiscreteAxisDescriptor):
+ elif hasattr(axisObject, "values"):
axisElement.attrib['values'] = " ".join(self.intOrFloat(v) for v in axisObject.values)
axisElement.attrib['default'] = self.intOrFloat(axisObject.default)
if axisObject.hidden:
@@ -1682,14 +1694,19 @@ class BaseDocWriter(object):
for subset in vf.axisSubsets:
subsetElement = ET.Element('axis-subset')
subsetElement.attrib['name'] = subset.name
- if isinstance(subset, RangeAxisSubsetDescriptor):
+ # Mypy doesn't support narrowing union types via hasattr()
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
+ # TODO(Python 3.10): use TypeGuard
+ if hasattr(subset, "userMinimum"):
+ subset = cast(RangeAxisSubsetDescriptor, subset)
if subset.userMinimum != -math.inf:
subsetElement.attrib['userminimum'] = self.intOrFloat(subset.userMinimum)
if subset.userMaximum != math.inf:
subsetElement.attrib['usermaximum'] = self.intOrFloat(subset.userMaximum)
if subset.userDefault is not None:
subsetElement.attrib['userdefault'] = self.intOrFloat(subset.userDefault)
- elif isinstance(subset, ValueAxisSubsetDescriptor):
+ elif hasattr(subset, "userValue"):
+ subset = cast(ValueAxisSubsetDescriptor, subset)
subsetElement.attrib['uservalue'] = self.intOrFloat(subset.userValue)
subsetsElement.append(subsetElement)
vfElement.append(subsetsElement)
@@ -2904,8 +2921,12 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
discreteAxes = []
rangeAxisSubsets: List[Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor]] = []
for axis in self.axes:
- if isinstance(axis, DiscreteAxisDescriptor):
- discreteAxes.append(axis)
+ if hasattr(axis, "values"):
+ # Mypy doesn't support narrowing union types via hasattr()
+ # TODO(Python 3.10): use TypeGuard
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
+ axis = cast(DiscreteAxisDescriptor, axis)
+ discreteAxes.append(axis) # type: ignore
else:
rangeAxisSubsets.append(RangeAxisSubsetDescriptor(name=axis.name))
valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
diff --git a/Lib/fontTools/designspaceLib/split.py b/Lib/fontTools/designspaceLib/split.py
index 2a09418c..408de70a 100644
--- a/Lib/fontTools/designspaceLib/split.py
+++ b/Lib/fontTools/designspaceLib/split.py
@@ -7,7 +7,7 @@ from __future__ import annotations
import itertools
import logging
import math
-from typing import Any, Callable, Dict, Iterator, List, Tuple
+from typing import Any, Callable, Dict, Iterator, List, Tuple, cast
from fontTools.designspaceLib import (
AxisDescriptor,
@@ -21,9 +21,9 @@ from fontTools.designspaceLib import (
)
from fontTools.designspaceLib.statNames import StatNames, getStatNames
from fontTools.designspaceLib.types import (
+ ConditionSet,
Range,
Region,
- ConditionSet,
getVFUserRegion,
locationInRegion,
regionInRegion,
@@ -87,11 +87,18 @@ def splitInterpolable(
discreteAxes = []
interpolableUserRegion: Region = {}
for axis in doc.axes:
- if isinstance(axis, DiscreteAxisDescriptor):
+ if hasattr(axis, "values"):
+ # Mypy doesn't support narrowing union types via hasattr()
+ # TODO(Python 3.10): use TypeGuard
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
+ axis = cast(DiscreteAxisDescriptor, axis)
discreteAxes.append(axis)
else:
+ axis = cast(AxisDescriptor, axis)
interpolableUserRegion[axis.name] = Range(
- axis.minimum, axis.maximum, axis.default
+ axis.minimum,
+ axis.maximum,
+ axis.default,
)
valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
for values in valueCombinations:
@@ -191,7 +198,11 @@ def _extractSubSpace(
for axis in doc.axes:
range = userRegion[axis.name]
- if isinstance(range, Range) and isinstance(axis, AxisDescriptor):
+ if isinstance(range, Range) and hasattr(axis, "minimum"):
+ # Mypy doesn't support narrowing union types via hasattr()
+ # TODO(Python 3.10): use TypeGuard
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
+ axis = cast(AxisDescriptor, axis)
subDoc.addAxis(
AxisDescriptor(
# Same info
diff --git a/Lib/fontTools/designspaceLib/statNames.py b/Lib/fontTools/designspaceLib/statNames.py
index 0a475c89..1b672703 100644
--- a/Lib/fontTools/designspaceLib/statNames.py
+++ b/Lib/fontTools/designspaceLib/statNames.py
@@ -88,21 +88,30 @@ def getStatNames(
# Then build names for all these languages, but fallback to English
# whenever a translation is missing.
labels = _getAxisLabelsForUserLocation(doc.axes, userLocation)
- languages = set(language for label in labels for language in label.labelNames)
- languages.add("en")
- for language in languages:
- styleName = " ".join(
- label.labelNames.get(language, label.defaultName)
- for label in labels
- if not label.elidable
- )
- if not styleName and doc.elidedFallbackName is not None:
- styleName = doc.elidedFallbackName
- styleNames[language] = styleName
+ if labels:
+ languages = set(language for label in labels for language in label.labelNames)
+ languages.add("en")
+ for language in languages:
+ styleName = " ".join(
+ label.labelNames.get(language, label.defaultName)
+ for label in labels
+ if not label.elidable
+ )
+ if not styleName and doc.elidedFallbackName is not None:
+ styleName = doc.elidedFallbackName
+ styleNames[language] = styleName
+
+ if "en" not in familyNames or "en" not in styleNames:
+ # Not enough information to compute PS names of styleMap names
+ return StatNames(
+ familyNames=familyNames,
+ styleNames=styleNames,
+ postScriptFontName=None,
+ styleMapFamilyNames={},
+ styleMapStyleName=None,
+ )
- postScriptFontName = None
- if "en" in familyNames and "en" in styleNames:
- postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")
+ postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")
styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation)
diff --git a/Lib/fontTools/designspaceLib/types.py b/Lib/fontTools/designspaceLib/types.py
index 8afea96c..80ba9d6d 100644
--- a/Lib/fontTools/designspaceLib/types.py
+++ b/Lib/fontTools/designspaceLib/types.py
@@ -1,12 +1,15 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import Dict, List, Optional, Union
+from typing import Dict, List, Optional, Union, cast
from fontTools.designspaceLib import (
+ AxisDescriptor,
DesignSpaceDocument,
+ DesignSpaceDocumentError,
RangeAxisSubsetDescriptor,
SimpleLocationDict,
+ ValueAxisSubsetDescriptor,
VariableFontDescriptor,
)
@@ -89,6 +92,10 @@ def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Re
designRegion = {}
for name, value in userRegion.items():
axis = doc.getAxis(name)
+ if axis is None:
+ raise DesignSpaceDocumentError(
+ f"Cannot find axis named '{name}' for region."
+ )
if isinstance(value, (float, int)):
designRegion[name] = axis.map_forward(value)
else:
@@ -107,16 +114,34 @@ def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Reg
# - it's a single location = use it to know which rules should apply in the VF
for axisSubset in vf.axisSubsets:
axis = doc.getAxis(axisSubset.name)
- if isinstance(axisSubset, RangeAxisSubsetDescriptor):
+ if axis is None:
+ raise DesignSpaceDocumentError(
+ f"Cannot find axis named '{axisSubset.name}' for variable font '{vf.name}'."
+ )
+ if hasattr(axisSubset, "userMinimum"):
+ # Mypy doesn't support narrowing union types via hasattr()
+ # TODO(Python 3.10): use TypeGuard
+ # https://mypy.readthedocs.io/en/stable/type_narrowing.html
+ axisSubset = cast(RangeAxisSubsetDescriptor, axisSubset)
+ if not hasattr(axis, "minimum"):
+ raise DesignSpaceDocumentError(
+ f"Cannot select a range over '{axis.name}' for variable font '{vf.name}' "
+ "because it's a discrete axis, use only 'userValue' instead."
+ )
+ axis = cast(AxisDescriptor, axis)
vfUserRegion[axis.name] = Range(
max(axisSubset.userMinimum, axis.minimum),
min(axisSubset.userMaximum, axis.maximum),
axisSubset.userDefault or axis.default,
)
else:
+ axisSubset = cast(ValueAxisSubsetDescriptor, axisSubset)
vfUserRegion[axis.name] = axisSubset.userValue
# Any axis not mentioned explicitly has a single location = default value
for axis in doc.axes:
if axis.name not in vfUserRegion:
+ assert isinstance(
+ axis.default, (int, float)
+ ), f"Axis '{axis.name}' has no valid default value."
vfUserRegion[axis.name] = axis.default
return vfUserRegion
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index a1644875..0a991761 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -230,8 +230,6 @@ class Builder(object):
self.font["GDEF"] = gdef
elif "GDEF" in self.font:
del self.font["GDEF"]
- elif self.varstorebuilder:
- raise FeatureLibError("Must save GDEF when compiling a variable font")
if "BASE" in tables:
base = self.buildBASE()
if base:
@@ -764,7 +762,7 @@ class Builder(object):
gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef else 0x00010000
if self.varstorebuilder:
store = self.varstorebuilder.finish()
- if store.VarData:
+ if store:
gdef.Version = 0x00010003
gdef.VarStore = store
varidx_map = store.optimize()
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index fd53573d..04ff6030 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -73,6 +73,7 @@ class Parser(object):
self.next_token_location_ = None
lexerClass = IncludingLexer if followIncludes else NonIncludingLexer
self.lexer_ = lexerClass(featurefile, includeDir=includeDir)
+ self.missing = {}
self.advance_lexer_(comments=True)
def parse(self):
@@ -125,6 +126,16 @@ class Parser(object):
),
self.cur_token_location_,
)
+ # Report any missing glyphs at the end of parsing
+ if self.missing:
+ error = [
+ " %s (first found at %s)" % (name, loc)
+ for name, loc in self.missing.items()
+ ]
+ raise FeatureLibError(
+ "The following glyph names are referenced but are missing from the "
+ "glyph set:\n" + ("\n".join(error)), None
+ )
return self.doc_
def parse_anchor_(self):
@@ -1242,14 +1253,6 @@ class Parser(object):
raise FeatureLibError(
"Name id value cannot be greater than 32767", self.cur_token_location_
)
- if 1 <= nameID <= 6:
- log.warning(
- "Name id %d cannot be set from the feature file. "
- "Ignoring record" % nameID
- )
- self.parse_name_() # skip to the next record
- return None
-
platformID, platEncID, langID, string = self.parse_name_()
return self.ast.NameRecord(
nameID, platformID, platEncID, langID, string, location=location
@@ -2073,19 +2076,18 @@ class Parser(object):
raise FeatureLibError("Expected a glyph name or CID", self.cur_token_location_)
def check_glyph_name_in_glyph_set(self, *names):
- """Raises if glyph name (just `start`) or glyph names of a
- range (`start` and `end`) are not in the glyph set.
+ """Adds a glyph name (just `start`) or glyph names of a
+ range (`start` and `end`) which are not in the glyph set
+ to the "missing list" for future error reporting.
If no glyph set is present, does nothing.
"""
if self.glyphNames_:
- missing = [name for name in names if name not in self.glyphNames_]
- if missing:
- raise FeatureLibError(
- "The following glyph names are referenced but are missing from the "
- f"glyph set: {', '.join(missing)}",
- self.cur_token_location_,
- )
+ for name in names:
+ if name in self.glyphNames_:
+ continue
+ if name not in self.missing:
+ self.missing[name] = self.cur_token_location_
def expect_markClass_reference_(self):
name = self.expect_class_name_()
diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py
index ad7180cb..60382683 100644
--- a/Lib/fontTools/fontBuilder.py
+++ b/Lib/fontTools/fontBuilder.py
@@ -838,6 +838,7 @@ class FontBuilder(object):
varStore=None,
varIndexMap=None,
clipBoxes=None,
+ allowLayerReuse=True,
):
"""Build new COLR table using color layers dictionary.
@@ -853,6 +854,7 @@ class FontBuilder(object):
varStore=varStore,
varIndexMap=varIndexMap,
clipBoxes=clipBoxes,
+ allowLayerReuse=allowLayerReuse,
)
def setupCPAL(
diff --git a/Lib/fontTools/merge/cmap.py b/Lib/fontTools/merge/cmap.py
index 7ade4ac9..7d98b588 100644
--- a/Lib/fontTools/merge/cmap.py
+++ b/Lib/fontTools/merge/cmap.py
@@ -18,10 +18,10 @@ def computeMegaGlyphOrder(merger, glyphOrders):
for i,glyphName in enumerate(glyphOrder):
if glyphName in megaOrder:
n = megaOrder[glyphName]
- while (glyphName + "#" + repr(n)) in megaOrder:
+ while (glyphName + "." + repr(n)) in megaOrder:
n += 1
megaOrder[glyphName] = n
- glyphName += "#" + repr(n)
+ glyphName += "." + repr(n)
glyphOrder[i] = glyphName
megaOrder[glyphName] = 1
merger.glyphOrder = megaOrder = list(megaOrder.keys())
diff --git a/Lib/fontTools/misc/cliTools.py b/Lib/fontTools/misc/cliTools.py
index e8c17677..e7dadf98 100644
--- a/Lib/fontTools/misc/cliTools.py
+++ b/Lib/fontTools/misc/cliTools.py
@@ -6,7 +6,7 @@ import re
numberAddedRE = re.compile(r"#\d+$")
-def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False):
+def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False, suffix=""):
"""Generates a suitable file name for writing output.
Often tools will want to take a file, do some kind of transformation to it,
@@ -14,6 +14,7 @@ def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False):
output file, through one or more of the following steps:
- changing the output directory
+ - appending suffix before file extension
- replacing the file extension
- suffixing the filename with a number (``#1``, ``#2``, etc.) to avoid
overwriting an existing file.
@@ -21,6 +22,8 @@ def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False):
Args:
input: Name of input file.
outputDir: Optionally, a new directory to write the file into.
+ suffix: Optionally, a string suffix is appended to file name before
+ the extension.
extension: Optionally, a replacement for the current file extension.
overWrite: Overwriting an existing file is permitted if true; if false
and the proposed filename exists, a new name will be generated by
@@ -36,11 +39,11 @@ def makeOutputFileName(input, outputDir=None, extension=None, overWrite=False):
fileName = numberAddedRE.split(fileName)[0]
if extension is None:
extension = os.path.splitext(input)[1]
- output = os.path.join(dirName, fileName + extension)
+ output = os.path.join(dirName, fileName + suffix + extension)
n = 1
if not overWrite:
while os.path.exists(output):
output = os.path.join(
- dirName, fileName + "#" + repr(n) + extension)
+ dirName, fileName + suffix + "#" + repr(n) + extension)
n += 1
return output
diff --git a/Lib/fontTools/misc/symfont.py b/Lib/fontTools/misc/symfont.py
index a1a87300..3ff2b5df 100644
--- a/Lib/fontTools/misc/symfont.py
+++ b/Lib/fontTools/misc/symfont.py
@@ -108,16 +108,34 @@ MomentYYPen = partial(GreenPen, func=y*y)
MomentXYPen = partial(GreenPen, func=x*y)
-def printGreenPen(penName, funcs, file=sys.stdout):
+def printGreenPen(penName, funcs, file=sys.stdout, docstring=None):
+
+ if docstring is not None:
+ print('"""%s"""' % docstring)
print(
-'''from fontTools.pens.basePen import BasePen
+'''from fontTools.pens.basePen import BasePen, OpenContourError
+try:
+ import cython
+except ImportError:
+ # if cython not installed, use mock module with no-op decorators and types
+ from fontTools.misc import cython
+
+if cython.compiled:
+ # Yep, I'm compiled.
+ COMPILED = True
+else:
+ # Just a lowly interpreted script.
+ COMPILED = False
+
+
+__all__ = ["%s"]
class %s(BasePen):
def __init__(self, glyphset=None):
BasePen.__init__(self, glyphset)
-'''%penName, file=file)
+'''% (penName, penName), file=file)
for name,f in funcs:
print(' self.%s = 0' % name, file=file)
print('''
@@ -133,41 +151,58 @@ class %s(BasePen):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
# Green theorem is not defined on open contours.
- raise NotImplementedError
+ raise OpenContourError(
+ "Green theorem is not defined on open contours."
+ )
''', end='', file=file)
for n in (1, 2, 3):
+
+ subs = {P[i][j]: [X, Y][j][i] for i in range(n+1) for j in range(2)}
+ greens = [green(f, BezierCurve[n]) for name,f in funcs]
+ greens = [sp.gcd_terms(f.collect(sum(P,()))) for f in greens] # Optimize
+ greens = [f.subs(subs) for f in greens] # Convert to p to x/y
+ defs, exprs = sp.cse(greens,
+ optimizations='basic',
+ symbols=(sp.Symbol('r%d'%i) for i in count()))
+
+ print()
+ for name,value in defs:
+ print(' @cython.locals(%s=cython.double)' % name, file=file)
if n == 1:
- print('''
+ print('''\
+ @cython.locals(x0=cython.double, y0=cython.double)
+ @cython.locals(x1=cython.double, y1=cython.double)
def _lineTo(self, p1):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
''', file=file)
elif n == 2:
- print('''
+ print('''\
+ @cython.locals(x0=cython.double, y0=cython.double)
+ @cython.locals(x1=cython.double, y1=cython.double)
+ @cython.locals(x2=cython.double, y2=cython.double)
def _qCurveToOne(self, p1, p2):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
''', file=file)
elif n == 3:
- print('''
+ print('''\
+ @cython.locals(x0=cython.double, y0=cython.double)
+ @cython.locals(x1=cython.double, y1=cython.double)
+ @cython.locals(x2=cython.double, y2=cython.double)
+ @cython.locals(x3=cython.double, y3=cython.double)
def _curveToOne(self, p1, p2, p3):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
x3,y3 = p3
''', file=file)
- subs = {P[i][j]: [X, Y][j][i] for i in range(n+1) for j in range(2)}
- greens = [green(f, BezierCurve[n]) for name,f in funcs]
- greens = [sp.gcd_terms(f.collect(sum(P,()))) for f in greens] # Optimize
- greens = [f.subs(subs) for f in greens] # Convert to p to x/y
- defs, exprs = sp.cse(greens,
- optimizations='basic',
- symbols=(sp.Symbol('r%d'%i) for i in count()))
for name,value in defs:
print(' %s = %s' % (name, value), file=file)
+
print(file=file)
for name,value in zip([f[0] for f in funcs], exprs):
print(' self.%s += %s' % (name, value), file=file)
diff --git a/Lib/fontTools/misc/treeTools.py b/Lib/fontTools/misc/treeTools.py
new file mode 100644
index 00000000..24e10ba5
--- /dev/null
+++ b/Lib/fontTools/misc/treeTools.py
@@ -0,0 +1,45 @@
+"""Generic tools for working with trees."""
+
+from math import ceil, log
+
+
+def build_n_ary_tree(leaves, n):
+ """Build N-ary tree from sequence of leaf nodes.
+
+ Return a list of lists where each non-leaf node is a list containing
+ max n nodes.
+ """
+ if not leaves:
+ return []
+
+ assert n > 1
+
+ depth = ceil(log(len(leaves), n))
+
+ if depth <= 1:
+ return list(leaves)
+
+ # Fully populate complete subtrees of root until we have enough leaves left
+ root = []
+ unassigned = None
+ full_step = n ** (depth - 1)
+ for i in range(0, len(leaves), full_step):
+ subtree = leaves[i : i + full_step]
+ if len(subtree) < full_step:
+ unassigned = subtree
+ break
+ while len(subtree) > n:
+ subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
+ root.append(subtree)
+
+ if unassigned:
+ # Recurse to fill the last subtree, which is the only partially populated one
+ subtree = build_n_ary_tree(unassigned, n)
+ if len(subtree) <= n - len(root):
+ # replace last subtree with its children if they can still fit
+ root.extend(subtree)
+ else:
+ root.append(subtree)
+ assert len(root) <= n
+
+ return root
diff --git a/Lib/fontTools/misc/visitor.py b/Lib/fontTools/misc/visitor.py
new file mode 100644
index 00000000..3d28135f
--- /dev/null
+++ b/Lib/fontTools/misc/visitor.py
@@ -0,0 +1,143 @@
+"""Generic visitor pattern implementation for Python objects."""
+
+import enum
+
+
+class Visitor(object):
+
+ defaultStop = False
+
+ @classmethod
+ def _register(celf, clazzes_attrs):
+ assert celf != Visitor, "Subclass Visitor instead."
+ if "_visitors" not in celf.__dict__:
+ celf._visitors = {}
+
+ def wrapper(method):
+ assert method.__name__ == "visit"
+ for clazzes, attrs in clazzes_attrs:
+ if type(clazzes) != tuple:
+ clazzes = (clazzes,)
+ if type(attrs) == str:
+ attrs = (attrs,)
+ for clazz in clazzes:
+ _visitors = celf._visitors.setdefault(clazz, {})
+ for attr in attrs:
+ assert attr not in _visitors, (
+ "Oops, class '%s' has visitor function for '%s' defined already."
+ % (clazz.__name__, attr)
+ )
+ _visitors[attr] = method
+ return None
+
+ return wrapper
+
+ @classmethod
+ def register(celf, clazzes):
+ if type(clazzes) != tuple:
+ clazzes = (clazzes,)
+ return celf._register([(clazzes, (None,))])
+
+ @classmethod
+ def register_attr(celf, clazzes, attrs):
+ clazzes_attrs = []
+ if type(clazzes) != tuple:
+ clazzes = (clazzes,)
+ if type(attrs) == str:
+ attrs = (attrs,)
+ for clazz in clazzes:
+ clazzes_attrs.append((clazz, attrs))
+ return celf._register(clazzes_attrs)
+
+ @classmethod
+ def register_attrs(celf, clazzes_attrs):
+ return celf._register(clazzes_attrs)
+
+ @classmethod
+ def _visitorsFor(celf, thing, _default={}):
+ typ = type(thing)
+
+ for celf in celf.mro():
+
+ _visitors = getattr(celf, "_visitors", None)
+ if _visitors is None:
+ break
+
+ m = celf._visitors.get(typ, None)
+ if m is not None:
+ return m
+
+ return _default
+
+ def visitObject(self, obj, *args, **kwargs):
+ """Called to visit an object. This function loops over all non-private
+ attributes of the objects and calls any user-registered (via
+ @register_attr() or @register_attrs()) visit() functions.
+
+ If there is no user-registered visit function, of if there is and it
+ returns True, or it returns None (or doesn't return anything) and
+ visitor.defaultStop is False (default), then the visitor will proceed
+ to call self.visitAttr()"""
+
+ keys = sorted(vars(obj).keys())
+ _visitors = self._visitorsFor(obj)
+ defaultVisitor = _visitors.get("*", None)
+ for key in keys:
+ if key[0] == "_":
+ continue
+ value = getattr(obj, key)
+ visitorFunc = _visitors.get(key, defaultVisitor)
+ if visitorFunc is not None:
+ ret = visitorFunc(self, obj, key, value, *args, **kwargs)
+ if ret == False or (ret is None and self.defaultStop):
+ continue
+ self.visitAttr(obj, key, value, *args, **kwargs)
+
+ def visitAttr(self, obj, attr, value, *args, **kwargs):
+ """Called to visit an attribute of an object."""
+ self.visit(value, *args, **kwargs)
+
+ def visitList(self, obj, *args, **kwargs):
+ """Called to visit any value that is a list."""
+ for value in obj:
+ self.visit(value, *args, **kwargs)
+
+ def visitDict(self, obj, *args, **kwargs):
+ """Called to visit any value that is a dictionary."""
+ for value in obj.values():
+ self.visit(value, *args, **kwargs)
+
+ def visitLeaf(self, obj, *args, **kwargs):
+ """Called to visit any value that is not an object, list,
+ or dictionary."""
+ pass
+
+ def visit(self, obj, *args, **kwargs):
+ """This is the main entry to the visitor. The visitor will visit object
+ obj.
+
+ The visitor will first determine if there is a registered (via
+ @register()) visit function for the type of object. If there is, it
+ will be called, and (visitor, obj, *args, **kwargs) will be passed to
+ the user visit function.
+
+ If there is no user-registered visit function, of if there is and it
+ returns True, or it returns None (or doesn't return anything) and
+ visitor.defaultStop is False (default), then the visitor will proceed
+ to dispatch to one of self.visitObject(), self.visitList(),
+ self.visitDict(), or self.visitLeaf() (any of which can be overriden in
+ a subclass)."""
+
+ visitorFunc = self._visitorsFor(obj).get(None, None)
+ if visitorFunc is not None:
+ ret = visitorFunc(self, obj, *args, **kwargs)
+ if ret == False or (ret is None and self.defaultStop):
+ return
+ if hasattr(obj, "__dict__") and not isinstance(obj, enum.Enum):
+ self.visitObject(obj, *args, **kwargs)
+ elif isinstance(obj, list):
+ self.visitList(obj, *args, **kwargs)
+ elif isinstance(obj, dict):
+ self.visitDict(obj, *args, **kwargs)
+ else:
+ self.visitLeaf(obj, *args, **kwargs)
diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py
index 667a216d..f117a742 100644
--- a/Lib/fontTools/mtiLib/__init__.py
+++ b/Lib/fontTools/mtiLib/__init__.py
@@ -121,7 +121,7 @@ def parseScriptList(lines, featureMap=None):
script = script[0].Script
else:
scriptRec = ot.ScriptRecord()
- scriptRec.ScriptTag = scriptTag
+ scriptRec.ScriptTag = scriptTag + ' '*(4 - len(scriptTag))
scriptRec.Script = ot.Script()
records.append(scriptRec)
script = scriptRec.Script
@@ -1165,7 +1165,7 @@ def build(f, font, tableTag=None):
def main(args=None, font=None):
- """Convert a FontDame OTL file to TTX XML.
+ """Convert a FontDame OTL file to TTX XML
Writes XML output to stdout.
diff --git a/Lib/fontTools/otlLib/optimize/__init__.py b/Lib/fontTools/otlLib/optimize/__init__.py
index a9512fb0..25bce9cd 100644
--- a/Lib/fontTools/otlLib/optimize/__init__.py
+++ b/Lib/fontTools/otlLib/optimize/__init__.py
@@ -4,7 +4,7 @@ from fontTools.ttLib import TTFont
def main(args=None):
- """Optimize the layout tables of an existing font."""
+ """Optimize the layout tables of an existing font"""
from argparse import ArgumentParser
from fontTools import configLogger
diff --git a/Lib/fontTools/pens/basePen.py b/Lib/fontTools/pens/basePen.py
index e06c00ef..f981f806 100644
--- a/Lib/fontTools/pens/basePen.py
+++ b/Lib/fontTools/pens/basePen.py
@@ -47,6 +47,9 @@ __all__ = ["AbstractPen", "NullPen", "BasePen", "PenError",
class PenError(Exception):
"""Represents an error during penning."""
+class OpenContourError(PenError):
+ pass
+
class AbstractPen:
diff --git a/Lib/fontTools/pens/cairoPen.py b/Lib/fontTools/pens/cairoPen.py
new file mode 100644
index 00000000..9cd5da91
--- /dev/null
+++ b/Lib/fontTools/pens/cairoPen.py
@@ -0,0 +1,26 @@
+"""Pen to draw to a Cairo graphics library context."""
+
+from fontTools.pens.basePen import BasePen
+
+
+__all__ = ["CairoPen"]
+
+
+class CairoPen(BasePen):
+ """Pen to draw to a Cairo graphics library context."""
+
+ def __init__(self, glyphSet, context):
+ BasePen.__init__(self, glyphSet)
+ self.context = context
+
+ def _moveTo(self, p):
+ self.context.move_to(*p)
+
+ def _lineTo(self, p):
+ self.context.line_to(*p)
+
+ def _curveToOne(self, p1, p2, p3):
+ self.context.curve_to(*p1, *p2, *p3)
+
+ def _closePath(self):
+ self.context.close_path()
diff --git a/Lib/fontTools/pens/momentsPen.py b/Lib/fontTools/pens/momentsPen.py
index 8c90f70a..7cd87919 100644
--- a/Lib/fontTools/pens/momentsPen.py
+++ b/Lib/fontTools/pens/momentsPen.py
@@ -1,14 +1,19 @@
-"""Pen calculating 0th, 1st, and 2nd moments of area of glyph shapes.
-This is low-level, autogenerated pen. Use statisticsPen instead."""
-from fontTools.pens.basePen import BasePen
+from fontTools.pens.basePen import BasePen, OpenContourError
+try:
+ import cython
+except ImportError:
+ # if cython not installed, use mock module with no-op decorators and types
+ from fontTools.misc import cython
+if cython.compiled:
+ # Yep, I'm compiled.
+ COMPILED = True
+else:
+ # Just a lowly interpreted script.
+ COMPILED = False
-__all__ = ["MomentsPen"]
-
-
-class OpenContourError(NotImplementedError):
- pass
+__all__ = ["MomentsPen"]
class MomentsPen(BasePen):
@@ -33,10 +38,26 @@ class MomentsPen(BasePen):
def _endPath(self):
p0 = self._getCurrentPoint()
if p0 != self.__startPoint:
+ # Green theorem is not defined on open contours.
raise OpenContourError(
"Green theorem is not defined on open contours."
)
+ @cython.locals(r0=cython.double)
+ @cython.locals(r1=cython.double)
+ @cython.locals(r2=cython.double)
+ @cython.locals(r3=cython.double)
+ @cython.locals(r4=cython.double)
+ @cython.locals(r5=cython.double)
+ @cython.locals(r6=cython.double)
+ @cython.locals(r7=cython.double)
+ @cython.locals(r8=cython.double)
+ @cython.locals(r9=cython.double)
+ @cython.locals(r10=cython.double)
+ @cython.locals(r11=cython.double)
+ @cython.locals(r12=cython.double)
+ @cython.locals(x0=cython.double, y0=cython.double)
+ @cython.locals(x1=cython.double, y1=cython.double)
def _lineTo(self, p1):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
@@ -44,246 +65,431 @@ class MomentsPen(BasePen):
r0 = x1*y0
r1 = x1*y1
r2 = x1**2
- r3 = x0**2
- r4 = 2*y0
- r5 = y0 - y1
- r6 = r5*x0
- r7 = y0**2
- r8 = y1**2
- r9 = x1**3
- r10 = r4*y1
+ r3 = r2*y1
+ r4 = y0 - y1
+ r5 = r4*x0
+ r6 = x0**2
+ r7 = 2*y0
+ r8 = y0**2
+ r9 = y1**2
+ r10 = x1**3
r11 = y0**3
r12 = y1**3
self.area += -r0/2 - r1/2 + x0*(y0 + y1)/2
- self.momentX += -r2*y0/6 - r2*y1/3 + r3*(r4 + y1)/6 - r6*x1/6
- self.momentY += -r0*y1/6 - r7*x1/6 - r8*x1/6 + x0*(r7 + r8 + y0*y1)/6
- self.momentXX += -r2*r6/12 - r3*r5*x1/12 - r9*y0/12 - r9*y1/4 + x0**3*(3*y0 + y1)/12
- self.momentXY += -r10*r2/24 - r2*r7/24 - r2*r8/8 + r3*(r10 + 3*r7 + r8)/24 - x0*x1*(r7 - r8)/12
- self.momentYY += -r0*r8/12 - r1*r7/12 - r11*x1/12 - r12*x1/12 + x0*(r11 + r12 + r7*y1 + r8*y0)/12
+ self.momentX += -r2*y0/6 - r3/3 - r5*x1/6 + r6*(r7 + y1)/6
+ self.momentY += -r0*y1/6 - r8*x1/6 - r9*x1/6 + x0*(r8 + r9 + y0*y1)/6
+ self.momentXX += -r10*y0/12 - r10*y1/4 - r2*r5/12 - r4*r6*x1/12 + x0**3*(3*y0 + y1)/12
+ self.momentXY += -r2*r8/24 - r2*r9/8 - r3*r7/24 + r6*(r7*y1 + 3*r8 + r9)/24 - x0*x1*(r8 - r9)/12
+ self.momentYY += -r0*r9/12 - r1*r8/12 - r11*x1/12 - r12*x1/12 + x0*(r11 + r12 + r8*y1 + r9*y0)/12
+ @cython.locals(r0=cython.double)
+ @cython.locals(r1=cython.double)
+ @cython.locals(r2=cython.double)
+ @cython.locals(r3=cython.double)
+ @cython.locals(r4=cython.double)
+ @cython.locals(r5=cython.double)
+ @cython.locals(r6=cython.double)
+ @cython.locals(r7=cython.double)
+ @cython.locals(r8=cython.double)
+ @cython.locals(r9=cython.double)
+ @cython.locals(r10=cython.double)
+ @cython.locals(r11=cython.double)
+ @cython.locals(r12=cython.double)
+ @cython.locals(r13=cython.double)
+ @cython.locals(r14=cython.double)
+ @cython.locals(r15=cython.double)
+ @cython.locals(r16=cython.double)
+ @cython.locals(r17=cython.double)
+ @cython.locals(r18=cython.double)
+ @cython.locals(r19=cython.double)
+ @cython.locals(r20=cython.double)
+ @cython.locals(r21=cython.double)
+ @cython.locals(r22=cython.double)
+ @cython.locals(r23=cython.double)
+ @cython.locals(r24=cython.double)
+ @cython.locals(r25=cython.double)
+ @cython.locals(r26=cython.double)
+ @cython.locals(r27=cython.double)
+ @cython.locals(r28=cython.double)
+ @cython.locals(r29=cython.double)
+ @cython.locals(r30=cython.double)
+ @cython.locals(r31=cython.double)
+ @cython.locals(r32=cython.double)
+ @cython.locals(r33=cython.double)
+ @cython.locals(r34=cython.double)
+ @cython.locals(r35=cython.double)
+ @cython.locals(r36=cython.double)
+ @cython.locals(r37=cython.double)
+ @cython.locals(r38=cython.double)
+ @cython.locals(r39=cython.double)
+ @cython.locals(r40=cython.double)
+ @cython.locals(r41=cython.double)
+ @cython.locals(r42=cython.double)
+ @cython.locals(r43=cython.double)
+ @cython.locals(r44=cython.double)
+ @cython.locals(r45=cython.double)
+ @cython.locals(r46=cython.double)
+ @cython.locals(r47=cython.double)
+ @cython.locals(r48=cython.double)
+ @cython.locals(r49=cython.double)
+ @cython.locals(r50=cython.double)
+ @cython.locals(r51=cython.double)
+ @cython.locals(r52=cython.double)
+ @cython.locals(r53=cython.double)
+ @cython.locals(x0=cython.double, y0=cython.double)
+ @cython.locals(x1=cython.double, y1=cython.double)
+ @cython.locals(x2=cython.double, y2=cython.double)
def _qCurveToOne(self, p1, p2):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
- r0 = 2*x1
- r1 = r0*y2
- r2 = 2*y1
- r3 = r2*x2
- r4 = 3*y2
- r5 = r4*x2
- r6 = 3*y0
- r7 = x1**2
- r8 = 2*y2
- r9 = x2**2
- r10 = 4*y1
- r11 = 10*y2
- r12 = r0*x2
- r13 = x0**2
- r14 = 10*y0
- r15 = x2*y2
- r16 = r0*y1 + r15
- r17 = 4*x1
- r18 = x2*y0
- r19 = r10*r15
- r20 = y1**2
- r21 = 2*r20
- r22 = y2**2
- r23 = r22*x2
- r24 = 5*r23
- r25 = y0**2
- r26 = y0*y2
- r27 = 5*r25
- r28 = 8*x1**3
- r29 = x2**3
- r30 = 30*y1
- r31 = 6*y1
- r32 = 10*r9*x1
- r33 = 4*r7
- r34 = 5*y2
- r35 = 12*r7
- r36 = r5 + 20*x1*y1
- r37 = 30*x1
- r38 = 12*x1
- r39 = 20*r7
- r40 = 8*r7*y1
- r41 = r34*r9
- r42 = 60*y1
- r43 = 20*r20
- r44 = 4*r20
- r45 = 15*r22
- r46 = r38*x2
- r47 = y1*y2
- r48 = 8*r20*x1 + r24
- r49 = 6*x1
- r50 = 8*y1**3
- r51 = y2**3
- r52 = y0**3
- r53 = 10*y1
- r54 = 12*y1
- r55 = 12*r20
+ r0 = 2*y1
+ r1 = r0*x2
+ r2 = x2*y2
+ r3 = 3*r2
+ r4 = 2*x1
+ r5 = 3*y0
+ r6 = x1**2
+ r7 = x2**2
+ r8 = 4*y1
+ r9 = 10*y2
+ r10 = 2*y2
+ r11 = r4*x2
+ r12 = x0**2
+ r13 = 10*y0
+ r14 = r4*y2
+ r15 = x2*y0
+ r16 = 4*x1
+ r17 = r0*x1 + r2
+ r18 = r2*r8
+ r19 = y1**2
+ r20 = 2*r19
+ r21 = y2**2
+ r22 = r21*x2
+ r23 = 5*r22
+ r24 = y0**2
+ r25 = y0*y2
+ r26 = 5*r24
+ r27 = x1**3
+ r28 = x2**3
+ r29 = 30*y1
+ r30 = 6*y1
+ r31 = 10*r7*x1
+ r32 = 5*y2
+ r33 = 12*r6
+ r34 = 30*x1
+ r35 = x1*y1
+ r36 = r3 + 20*r35
+ r37 = 12*x1
+ r38 = 20*r6
+ r39 = 8*r6*y1
+ r40 = r32*r7
+ r41 = 60*y1
+ r42 = 20*r19
+ r43 = 4*r19
+ r44 = 15*r21
+ r45 = 12*x2
+ r46 = 12*y2
+ r47 = 6*x1
+ r48 = 8*r19*x1 + r23
+ r49 = 8*y1**3
+ r50 = y2**3
+ r51 = y0**3
+ r52 = 10*y1
+ r53 = 12*y1
- self.area += r1/6 - r3/6 - r5/6 + x0*(r2 + r6 + y2)/6 - y0*(r0 + x2)/6
- self.momentX += -r10*r9/30 - r11*r9/30 - r12*(-r8 + y1)/30 + r13*(r10 + r14 + y2)/30 + r7*r8/30 + x0*(r1 + r16 - r17*y0 - r18)/30 - y0*(r12 + 2*r7 + r9)/30
- self.momentY += r1*(r8 + y1)/30 - r19/30 - r21*x2/30 - r24/30 - r25*(r17 + x2)/30 + x0*(r10*y0 + r2*y2 + r21 + r22 + r26 + r27)/30 - y0*(r16 + r3)/30
- self.momentXX += r13*(r11*x1 - 5*r18 + r3 + r36 - r37*y0)/420 + r28*y2/420 - r29*r30/420 - r29*y2/4 - r32*(r2 - r4)/420 - r33*x2*(r2 - r34)/420 + x0**3*(r31 + 21*y0 + y2)/84 - x0*(-r15*r38 + r18*r38 + r2*r9 - r35*y2 + r39*y0 - r40 - r41 + r6*r9)/420 - y0*(r28 + 5*r29 + r32 + r35*x2)/420
- self.momentXY += r13*(r14*y2 + 3*r22 + 105*r25 + r42*y0 + r43 + 12*r47)/840 - r17*x2*(r44 - r45)/840 - r22*r9/8 - r25*(r39 + r46 + 3*r9)/840 + r33*y2*(r10 + r34)/840 - r42*r9*y2/840 - r43*r9/840 + x0*(-r10*r18 + r17*r26 + r19 + r22*r49 - r25*r37 - r27*x2 + r38*r47 + r48)/420 - y0*(r15*r17 + r31*r9 + r40 + r41 + r46*y1)/420
- self.momentYY += r1*(r11*y1 + r44 + r45)/420 - r15*r43/420 - r23*r30/420 - r25*(r1 + r36 + r53*x2)/420 - r50*x2/420 - r51*x2/12 - r52*(r49 + x2)/84 + x0*(r22*r53 + r22*r6 + r25*r30 + r25*r34 + r26*r54 + r43*y0 + r50 + 5*r51 + 35*r52 + r55*y2)/420 - y0*(-r0*r22 + r15*r54 + r48 + r55*x2)/420
+ self.area += -r1/6 - r3/6 + x0*(r0 + r5 + y2)/6 + x1*y2/3 - y0*(r4 + x2)/6
+ self.momentX += -r11*(-r10 + y1)/30 + r12*(r13 + r8 + y2)/30 + r6*y2/15 - r7*r8/30 - r7*r9/30 + x0*(r14 - r15 - r16*y0 + r17)/30 - y0*(r11 + 2*r6 + r7)/30
+ self.momentY += -r18/30 - r20*x2/30 - r23/30 - r24*(r16 + x2)/30 + x0*(r0*y2 + r20 + r21 + r25 + r26 + r8*y0)/30 + x1*y2*(r10 + y1)/15 - y0*(r1 + r17)/30
+ self.momentXX += r12*(r1 - 5*r15 - r34*y0 + r36 + r9*x1)/420 + 2*r27*y2/105 - r28*r29/420 - r28*y2/4 - r31*(r0 - 3*y2)/420 - r6*x2*(r0 - r32)/105 + x0**3*(r30 + 21*y0 + y2)/84 - x0*(r0*r7 + r15*r37 - r2*r37 - r33*y2 + r38*y0 - r39 - r40 + r5*r7)/420 - y0*(8*r27 + 5*r28 + r31 + r33*x2)/420
+ self.momentXY += r12*(r13*y2 + 3*r21 + 105*r24 + r41*y0 + r42 + r46*y1)/840 - r16*x2*(r43 - r44)/840 - r21*r7/8 - r24*(r38 + r45*x1 + 3*r7)/840 - r41*r7*y2/840 - r42*r7/840 + r6*y2*(r32 + r8)/210 + x0*(-r15*r8 + r16*r25 + r18 + r21*r47 - r24*r34 - r26*x2 + r35*r46 + r48)/420 - y0*(r16*r2 + r30*r7 + r35*r45 + r39 + r40)/420
+ self.momentYY += -r2*r42/420 - r22*r29/420 - r24*(r14 + r36 + r52*x2)/420 - r49*x2/420 - r50*x2/12 - r51*(r47 + x2)/84 + x0*(r19*r46 + r21*r5 + r21*r52 + r24*r29 + r25*r53 + r26*y2 + r42*y0 + r49 + 5*r50 + 35*r51)/420 + x1*y2*(r43 + r44 + r9*y1)/210 - y0*(r19*r45 + r2*r53 - r21*r4 + r48)/420
+ @cython.locals(r0=cython.double)
+ @cython.locals(r1=cython.double)
+ @cython.locals(r2=cython.double)
+ @cython.locals(r3=cython.double)
+ @cython.locals(r4=cython.double)
+ @cython.locals(r5=cython.double)
+ @cython.locals(r6=cython.double)
+ @cython.locals(r7=cython.double)
+ @cython.locals(r8=cython.double)
+ @cython.locals(r9=cython.double)
+ @cython.locals(r10=cython.double)
+ @cython.locals(r11=cython.double)
+ @cython.locals(r12=cython.double)
+ @cython.locals(r13=cython.double)
+ @cython.locals(r14=cython.double)
+ @cython.locals(r15=cython.double)
+ @cython.locals(r16=cython.double)
+ @cython.locals(r17=cython.double)
+ @cython.locals(r18=cython.double)
+ @cython.locals(r19=cython.double)
+ @cython.locals(r20=cython.double)
+ @cython.locals(r21=cython.double)
+ @cython.locals(r22=cython.double)
+ @cython.locals(r23=cython.double)
+ @cython.locals(r24=cython.double)
+ @cython.locals(r25=cython.double)
+ @cython.locals(r26=cython.double)
+ @cython.locals(r27=cython.double)
+ @cython.locals(r28=cython.double)
+ @cython.locals(r29=cython.double)
+ @cython.locals(r30=cython.double)
+ @cython.locals(r31=cython.double)
+ @cython.locals(r32=cython.double)
+ @cython.locals(r33=cython.double)
+ @cython.locals(r34=cython.double)
+ @cython.locals(r35=cython.double)
+ @cython.locals(r36=cython.double)
+ @cython.locals(r37=cython.double)
+ @cython.locals(r38=cython.double)
+ @cython.locals(r39=cython.double)
+ @cython.locals(r40=cython.double)
+ @cython.locals(r41=cython.double)
+ @cython.locals(r42=cython.double)
+ @cython.locals(r43=cython.double)
+ @cython.locals(r44=cython.double)
+ @cython.locals(r45=cython.double)
+ @cython.locals(r46=cython.double)
+ @cython.locals(r47=cython.double)
+ @cython.locals(r48=cython.double)
+ @cython.locals(r49=cython.double)
+ @cython.locals(r50=cython.double)
+ @cython.locals(r51=cython.double)
+ @cython.locals(r52=cython.double)
+ @cython.locals(r53=cython.double)
+ @cython.locals(r54=cython.double)
+ @cython.locals(r55=cython.double)
+ @cython.locals(r56=cython.double)
+ @cython.locals(r57=cython.double)
+ @cython.locals(r58=cython.double)
+ @cython.locals(r59=cython.double)
+ @cython.locals(r60=cython.double)
+ @cython.locals(r61=cython.double)
+ @cython.locals(r62=cython.double)
+ @cython.locals(r63=cython.double)
+ @cython.locals(r64=cython.double)
+ @cython.locals(r65=cython.double)
+ @cython.locals(r66=cython.double)
+ @cython.locals(r67=cython.double)
+ @cython.locals(r68=cython.double)
+ @cython.locals(r69=cython.double)
+ @cython.locals(r70=cython.double)
+ @cython.locals(r71=cython.double)
+ @cython.locals(r72=cython.double)
+ @cython.locals(r73=cython.double)
+ @cython.locals(r74=cython.double)
+ @cython.locals(r75=cython.double)
+ @cython.locals(r76=cython.double)
+ @cython.locals(r77=cython.double)
+ @cython.locals(r78=cython.double)
+ @cython.locals(r79=cython.double)
+ @cython.locals(r80=cython.double)
+ @cython.locals(r81=cython.double)
+ @cython.locals(r82=cython.double)
+ @cython.locals(r83=cython.double)
+ @cython.locals(r84=cython.double)
+ @cython.locals(r85=cython.double)
+ @cython.locals(r86=cython.double)
+ @cython.locals(r87=cython.double)
+ @cython.locals(r88=cython.double)
+ @cython.locals(r89=cython.double)
+ @cython.locals(r90=cython.double)
+ @cython.locals(r91=cython.double)
+ @cython.locals(r92=cython.double)
+ @cython.locals(r93=cython.double)
+ @cython.locals(r94=cython.double)
+ @cython.locals(r95=cython.double)
+ @cython.locals(r96=cython.double)
+ @cython.locals(r97=cython.double)
+ @cython.locals(r98=cython.double)
+ @cython.locals(r99=cython.double)
+ @cython.locals(r100=cython.double)
+ @cython.locals(r101=cython.double)
+ @cython.locals(r102=cython.double)
+ @cython.locals(r103=cython.double)
+ @cython.locals(r104=cython.double)
+ @cython.locals(r105=cython.double)
+ @cython.locals(r106=cython.double)
+ @cython.locals(r107=cython.double)
+ @cython.locals(r108=cython.double)
+ @cython.locals(r109=cython.double)
+ @cython.locals(r110=cython.double)
+ @cython.locals(r111=cython.double)
+ @cython.locals(r112=cython.double)
+ @cython.locals(r113=cython.double)
+ @cython.locals(r114=cython.double)
+ @cython.locals(r115=cython.double)
+ @cython.locals(r116=cython.double)
+ @cython.locals(r117=cython.double)
+ @cython.locals(r118=cython.double)
+ @cython.locals(r119=cython.double)
+ @cython.locals(r120=cython.double)
+ @cython.locals(r121=cython.double)
+ @cython.locals(r122=cython.double)
+ @cython.locals(r123=cython.double)
+ @cython.locals(r124=cython.double)
+ @cython.locals(r125=cython.double)
+ @cython.locals(r126=cython.double)
+ @cython.locals(r127=cython.double)
+ @cython.locals(r128=cython.double)
+ @cython.locals(r129=cython.double)
+ @cython.locals(r130=cython.double)
+ @cython.locals(r131=cython.double)
+ @cython.locals(r132=cython.double)
+ @cython.locals(x0=cython.double, y0=cython.double)
+ @cython.locals(x1=cython.double, y1=cython.double)
+ @cython.locals(x2=cython.double, y2=cython.double)
+ @cython.locals(x3=cython.double, y3=cython.double)
def _curveToOne(self, p1, p2, p3):
x0,y0 = self._getCurrentPoint()
x1,y1 = p1
x2,y2 = p2
x3,y3 = p3
- r0 = 6*x2
- r1 = r0*y3
- r2 = 6*y2
- r3 = 10*y3
- r4 = r3*x3
- r5 = 3*x1
- r6 = 3*y1
- r7 = 6*x1
- r8 = 3*x2
- r9 = 6*y1
- r10 = 3*y2
- r11 = x2**2
- r12 = r11*y3
- r13 = 45*r12
- r14 = x3**2
- r15 = r14*y2
- r16 = r14*y3
- r17 = x2*x3
- r18 = 15*r17
- r19 = 7*y3
- r20 = x1**2
- r21 = 9*r20
- r22 = x0**2
- r23 = 21*y1
- r24 = 9*r11
- r25 = 9*x2
- r26 = x2*y3
- r27 = 15*r26
- r28 = -r25*y1 + r27
- r29 = r25*y2
- r30 = r9*x3
- r31 = 45*x1
- r32 = x1*x3
- r33 = 45*r20
- r34 = 5*r14
- r35 = x2*y2
- r36 = 18*r35
- r37 = 5*x3
- r38 = r37*y3
- r39 = r31*y1 + r36 + r38
- r40 = x1*y0
- r41 = x1*y3
- r42 = x2*y0
- r43 = x3*y1
- r44 = r10*x3
- r45 = x3*y2*y3
- r46 = y2**2
- r47 = 45*r46
- r48 = r47*x3
- r49 = y3**2
+ r0 = 6*y2
+ r1 = r0*x3
+ r2 = 10*y3
+ r3 = r2*x3
+ r4 = 3*y1
+ r5 = 6*x1
+ r6 = 3*x2
+ r7 = 6*y1
+ r8 = 3*y2
+ r9 = x2**2
+ r10 = 45*r9
+ r11 = r10*y3
+ r12 = x3**2
+ r13 = r12*y2
+ r14 = r12*y3
+ r15 = 7*y3
+ r16 = 15*x3
+ r17 = r16*x2
+ r18 = x1**2
+ r19 = 9*r18
+ r20 = x0**2
+ r21 = 21*y1
+ r22 = 9*r9
+ r23 = r7*x3
+ r24 = 9*y2
+ r25 = r24*x2 + r3
+ r26 = 9*x2
+ r27 = x2*y3
+ r28 = -r26*y1 + 15*r27
+ r29 = 3*x1
+ r30 = 45*x1
+ r31 = 12*x3
+ r32 = 45*r18
+ r33 = 5*r12
+ r34 = r8*x3
+ r35 = 105*y0
+ r36 = 30*y0
+ r37 = r36*x2
+ r38 = 5*x3
+ r39 = 15*y3
+ r40 = 5*y3
+ r41 = r40*x3
+ r42 = x2*y2
+ r43 = 18*r42
+ r44 = 45*y1
+ r45 = r41 + r43 + r44*x1
+ r46 = y2*y3
+ r47 = r46*x3
+ r48 = y2**2
+ r49 = 45*r48
r50 = r49*x3
- r51 = y1**2
- r52 = 9*r51
- r53 = y0**2
- r54 = 21*x1
- r55 = x3*y2
- r56 = 15*r55
- r57 = 9*y2
- r58 = y2*y3
- r59 = 15*r58
- r60 = 9*r46
- r61 = 3*y3
- r62 = 45*y1
- r63 = r8*y3
- r64 = y0*y1
- r65 = y0*y2
- r66 = 30*r65
- r67 = 5*y3
- r68 = y1*y3
- r69 = 45*r51
- r70 = 5*r49
- r71 = x2**3
- r72 = x3**3
- r73 = 126*x3
- r74 = x1**3
- r75 = r14*x2
- r76 = 63*r11
- r77 = r76*x3
- r78 = 15*r35
- r79 = r19*x3
- r80 = x1*y1
- r81 = 63*r35
- r82 = r38 + 378*r80 + r81
- r83 = x1*y2
- r84 = x2*y1
- r85 = x3*y0
- r86 = x2*x3*y1
- r87 = x2*x3*y3
- r88 = r11*y2
- r89 = 27*r88
- r90 = 42*y3
- r91 = r14*r90
- r92 = 90*x1*x2
- r93 = 189*x2
- r94 = 30*x1*x3
- r95 = 14*r16 + 126*r20*y1 + 45*r88 + r94*y2
- r96 = x1*x2
- r97 = 252*r96
- r98 = x1*x2*y2
- r99 = 42*r32
- r100 = x1*x3*y1
- r101 = 30*r17
- r102 = 18*r17
- r103 = 378*r20
- r104 = 189*y2
- r105 = r20*y3
- r106 = r11*y1
- r107 = r14*y1
- r108 = 378*r46
- r109 = 252*y2
- r110 = y1*y2
- r111 = x2*x3*y2
- r112 = y0*y3
- r113 = 378*r51
- r114 = 63*r46
- r115 = 27*x2
- r116 = r115*r46 + 42*r50
- r117 = x2*y1*y3
- r118 = x3*y1*y2
- r119 = r49*x2
- r120 = r51*x3
- r121 = x3*y3
- r122 = 14*x3
- r123 = 30*r117 + r122*r49 + r47*x2 + 126*r51*x1
- r124 = x1*y1*y3
- r125 = x1*y2*y3
- r126 = x2*y1*y2
- r127 = 54*y3
- r128 = 21*r55
- r129 = 630*r53
- r130 = r46*x1
- r131 = r49*x1
- r132 = 126*r53
- r133 = y2**3
- r134 = y3**3
- r135 = 630*r49
- r136 = y1**3
- r137 = y0**3
- r138 = r114*y3 + r23*r49
- r139 = r49*y2
+ r51 = y3**2
+ r52 = r51*x3
+ r53 = y1**2
+ r54 = 9*r53
+ r55 = y0**2
+ r56 = 21*x1
+ r57 = 6*x2
+ r58 = r16*y2
+ r59 = r39*y2
+ r60 = 9*r48
+ r61 = r6*y3
+ r62 = 3*y3
+ r63 = r36*y2
+ r64 = y1*y3
+ r65 = 45*r53
+ r66 = 5*r51
+ r67 = x2**3
+ r68 = x3**3
+ r69 = 630*y2
+ r70 = 126*x3
+ r71 = x1**3
+ r72 = 126*x2
+ r73 = 63*r9
+ r74 = r73*x3
+ r75 = r15*x3 + 15*r42
+ r76 = 630*x1
+ r77 = 14*x3
+ r78 = 21*r27
+ r79 = 42*x1
+ r80 = 42*x2
+ r81 = x1*y2
+ r82 = 63*r42
+ r83 = x1*y1
+ r84 = r41 + r82 + 378*r83
+ r85 = x2*x3
+ r86 = r85*y1
+ r87 = r27*x3
+ r88 = 27*r9
+ r89 = r88*y2
+ r90 = 42*r14
+ r91 = 90*x1
+ r92 = 189*r18
+ r93 = 378*r18
+ r94 = r12*y1
+ r95 = 252*x1*x2
+ r96 = r79*x3
+ r97 = 30*r85
+ r98 = r83*x3
+ r99 = 30*x3
+ r100 = 42*x3
+ r101 = r42*x1
+ r102 = r10*y2 + 14*r14 + 126*r18*y1 + r81*r99
+ r103 = 378*r48
+ r104 = 18*y1
+ r105 = r104*y2
+ r106 = y0*y1
+ r107 = 252*y2
+ r108 = r107*y0
+ r109 = y0*y3
+ r110 = 42*r64
+ r111 = 378*r53
+ r112 = 63*r48
+ r113 = 27*x2
+ r114 = r27*y2
+ r115 = r113*r48 + 42*r52
+ r116 = x3*y3
+ r117 = 54*r42
+ r118 = r51*x1
+ r119 = r51*x2
+ r120 = r48*x1
+ r121 = 21*x3
+ r122 = r64*x1
+ r123 = r81*y3
+ r124 = 30*r27*y1 + r49*x2 + 14*r52 + 126*r53*x1
+ r125 = y2**3
+ r126 = y3**3
+ r127 = y1**3
+ r128 = y0**3
+ r129 = r51*y2
+ r130 = r112*y3 + r21*r51
+ r131 = 189*r53
+ r132 = 90*y2
- self.area += r1/20 - r2*x3/20 - r4/20 + r5*(y2 + y3)/20 - r6*(x2 + x3)/20 + x0*(r10 + r9 + 10*y0 + y3)/20 - y0*(r7 + r8 + x3)/20
- self.momentX += r13/840 - r15/8 - r16/3 - r18*(r10 - r19)/840 + r21*(r10 + 2*y3)/840 + r22*(r2 + r23 + 56*y0 + y3)/168 + r5*(r28 + r29 - r30 + r4)/840 - r6*(10*r14 + r18 + r24)/840 + x0*(12*r26 + r31*y2 - r37*y0 + r39 - 105*r40 + 15*r41 - 30*r42 - 3*r43 + r44)/840 - y0*(18*r11 + r18 + r31*x2 + 12*r32 + r33 + r34)/840
- self.momentY += r27*(r10 + r19)/840 - r45/8 - r48/840 + r5*(10*r49 + r57*y1 + r59 + r60 + r9*y3)/840 - r50/6 - r52*(r8 + 2*x3)/840 - r53*(r0 + r54 + x3)/168 - r6*(r29 + r4 + r56)/840 + x0*(18*r46 + 140*r53 + r59 + r62*y2 + 105*r64 + r66 + r67*y0 + 12*r68 + r69 + r70)/840 - y0*(r39 + 15*r43 + 12*r55 - r61*x1 + r62*x2 + r63)/840
- self.momentXX += -r11*r73*(-r61 + y2)/9240 + r21*(r28 - r37*y1 + r44 + r78 + r79)/9240 + r22*(21*r26 - 630*r40 + 42*r41 - 126*r42 + r57*x3 + r82 + 210*r83 + 42*r84 - 14*r85)/9240 - r5*(r11*r62 + r14*r23 + 14*r15 - r76*y3 + 54*r86 - 84*r87 - r89 - r91)/9240 - r6*(27*r71 + 42*r72 + 70*r75 + r77)/9240 + 3*r71*y3/220 - 3*r72*y2/44 - r72*y3/4 + 3*r74*(r57 + r67)/3080 - r75*(378*y2 - 630*y3)/9240 + x0**3*(r57 + r62 + 165*y0 + y3)/660 + x0*(-18*r100 - r101*y0 - r101*y1 + r102*y2 - r103*y0 + r104*r20 + 63*r105 - 27*r106 - 9*r107 + r13 - r34*y0 - r76*y0 + 42*r87 + r92*y3 + r94*y3 + r95 - r97*y0 + 162*r98 - r99*y0)/9240 - y0*(135*r11*x1 + r14*r54 + r20*r93 + r33*x3 + 45*r71 + 14*r72 + 126*r74 + 42*r75 + r77 + r92*x3)/9240
- self.momentXY += -r108*r14/18480 + r12*(r109 + 378*y3)/18480 - r14*r49/8 - 3*r14*r58/44 - r17*(252*r46 - 1260*r49)/18480 + r21*(18*r110 + r3*y1 + 15*r46 + 7*r49 + 18*r58)/18480 + r22*(252*r110 + 28*r112 + r113 + r114 + 2310*r53 + 30*r58 + 1260*r64 + 252*r65 + 42*r68 + r70)/18480 - r52*(r102 + 15*r11 + 7*r14)/18480 - r53*(r101 + r103 + r34 + r76 + r97 + r99)/18480 + r7*(-r115*r51 + r116 + 18*r117 - 18*r118 + 42*r119 - 15*r120 + 28*r45 + r81*y3)/18480 - r9*(63*r111 + 42*r15 + 28*r87 + r89 + r91)/18480 + x0*(r1*y0 + r104*r80 + r112*r54 + 21*r119 - 9*r120 - r122*r53 + r123 + 54*r124 + 60*r125 + 54*r126 + r127*r35 + r128*y3 - r129*x1 + 81*r130 + 15*r131 - r132*x2 - r2*r85 - r23*r85 + r30*y3 + 84*r40*y2 - 84*r42*y1 + r60*x3)/9240 - y0*(54*r100 - 9*r105 + 81*r106 + 15*r107 + 54*r111 + r121*r7 + 21*r15 + r24*y3 + 60*r86 + 21*r87 + r95 + 189*r96*y1 + 54*r98)/9240
- self.momentYY += -r108*r121/9240 - r133*r73/9240 - r134*x3/12 - r135*r55/9240 - 3*r136*(r25 + r37)/3080 - r137*(r25 + r31 + x3)/660 + r26*(r135 + 126*r46 + 378*y2*y3)/9240 + r5*(r110*r127 + 27*r133 + 42*r134 + r138 + 70*r139 + r46*r62 + 27*r51*y2 + 15*r51*y3)/9240 - r52*(r56 + r63 + r78 + r79)/9240 - r53*(r128 + r25*y3 + 42*r43 + r82 + 42*r83 + 210*r84)/9240 - r6*(r114*x3 + r116 - 14*r119 + 84*r45)/9240 + x0*(r104*r51 + r109*r64 + 90*r110*y3 + r113*y0 + r114*y0 + r129*y1 + r132*y2 + 45*r133 + 14*r134 + 126*r136 + 770*r137 + r138 + 42*r139 + 135*r46*y1 + 14*r53*y3 + r64*r90 + r66*y3 + r69*y3 + r70*y0)/9240 - y0*(90*r118 + 63*r120 + r123 - 18*r124 - 30*r125 + 162*r126 - 27*r130 - 9*r131 + r36*y3 + 30*r43*y3 + 42*r45 + r48 + r51*r93)/9240
+ self.area += -r1/20 - r3/20 - r4*(x2 + x3)/20 + x0*(r7 + r8 + 10*y0 + y3)/20 + 3*x1*(y2 + y3)/20 + 3*x2*y3/10 - y0*(r5 + r6 + x3)/20
+ self.momentX += r11/840 - r13/8 - r14/3 - r17*(-r15 + r8)/840 + r19*(r8 + 2*y3)/840 + r20*(r0 + r21 + 56*y0 + y3)/168 + r29*(-r23 + r25 + r28)/840 - r4*(10*r12 + r17 + r22)/840 + x0*(12*r27 + r30*y2 + r34 - r35*x1 - r37 - r38*y0 + r39*x1 - r4*x3 + r45)/840 - y0*(r17 + r30*x2 + r31*x1 + r32 + r33 + 18*r9)/840
+ self.momentY += -r4*(r25 + r58)/840 - r47/8 - r50/840 - r52/6 - r54*(r6 + 2*x3)/840 - r55*(r56 + r57 + x3)/168 + x0*(r35*y1 + r40*y0 + r44*y2 + 18*r48 + 140*r55 + r59 + r63 + 12*r64 + r65 + r66)/840 + x1*(r24*y1 + 10*r51 + r59 + r60 + r7*y3)/280 + x2*y3*(r15 + r8)/56 - y0*(r16*y1 + r31*y2 + r44*x2 + r45 + r61 - r62*x1)/840
+ self.momentXX += -r12*r72*(-r40 + r8)/9240 + 3*r18*(r28 + r34 - r38*y1 + r75)/3080 + r20*(r24*x3 - r72*y0 - r76*y0 - r77*y0 + r78 + r79*y3 + r80*y1 + 210*r81 + r84)/9240 - r29*(r12*r21 + 14*r13 + r44*r9 - r73*y3 + 54*r86 - 84*r87 - r89 - r90)/9240 - r4*(70*r12*x2 + 27*r67 + 42*r68 + r74)/9240 + 3*r67*y3/220 - r68*r69/9240 - r68*y3/4 - r70*r9*(-r62 + y2)/9240 + 3*r71*(r24 + r40)/3080 + x0**3*(r24 + r44 + 165*y0 + y3)/660 + x0*(r100*r27 + 162*r101 + r102 + r11 + 63*r18*y3 + r27*r91 - r33*y0 - r37*x3 + r43*x3 - r73*y0 - r88*y1 + r92*y2 - r93*y0 - 9*r94 - r95*y0 - r96*y0 - r97*y1 - 18*r98 + r99*x1*y3)/9240 - y0*(r12*r56 + r12*r80 + r32*x3 + 45*r67 + 14*r68 + 126*r71 + r74 + r85*r91 + 135*r9*x1 + r92*x2)/9240
+ self.momentXY += -r103*r12/18480 - r12*r51/8 - 3*r14*y2/44 + 3*r18*(r105 + r2*y1 + 18*r46 + 15*r48 + 7*r51)/6160 + r20*(1260*r106 + r107*y1 + r108 + 28*r109 + r110 + r111 + r112 + 30*r46 + 2310*r55 + r66)/18480 - r54*(7*r12 + 18*r85 + 15*r9)/18480 - r55*(r33 + r73 + r93 + r95 + r96 + r97)/18480 - r7*(42*r13 + r82*x3 + 28*r87 + r89 + r90)/18480 - 3*r85*(r48 - r66)/220 + 3*r9*y3*(r62 + 2*y2)/440 + x0*(-r1*y0 - 84*r106*x2 + r109*r56 + 54*r114 + r117*y1 + 15*r118 + 21*r119 + 81*r120 + r121*r46 + 54*r122 + 60*r123 + r124 - r21*x3*y0 + r23*y3 - r54*x3 - r55*r72 - r55*r76 - r55*r77 + r57*y0*y3 + r60*x3 + 84*r81*y0 + 189*r81*y1)/9240 + x1*(r104*r27 - r105*x3 - r113*r53 + 63*r114 + r115 - r16*r53 + 28*r47 + r51*r80)/3080 - y0*(54*r101 + r102 + r116*r5 + r117*x3 + 21*r13 - r19*y3 + r22*y3 + r78*x3 + 189*r83*x2 + 60*r86 + 81*r9*y1 + 15*r94 + 54*r98)/9240
+ self.momentYY += -r103*r116/9240 - r125*r70/9240 - r126*x3/12 - 3*r127*(r26 + r38)/3080 - r128*(r26 + r30 + x3)/660 - r4*(r112*x3 + r115 - 14*r119 + 84*r47)/9240 - r52*r69/9240 - r54*(r58 + r61 + r75)/9240 - r55*(r100*y1 + r121*y2 + r26*y3 + r79*y2 + r84 + 210*x2*y1)/9240 + x0*(r108*y1 + r110*y0 + r111*y0 + r112*y0 + 45*r125 + 14*r126 + 126*r127 + 770*r128 + 42*r129 + r130 + r131*y2 + r132*r64 + 135*r48*y1 + 630*r55*y1 + 126*r55*y2 + 14*r55*y3 + r63*y3 + r65*y3 + r66*y0)/9240 + x1*(27*r125 + 42*r126 + 70*r129 + r130 + r39*r53 + r44*r48 + 27*r53*y2 + 54*r64*y2)/3080 + 3*x2*y3*(r48 + r66 + r8*y3)/220 - y0*(r100*r46 + 18*r114 - 9*r118 - 27*r120 - 18*r122 - 30*r123 + r124 + r131*x2 + r132*x3*y1 + 162*r42*y1 + r50 + 63*r53*x3 + r64*r99)/9240
if __name__ == '__main__':
from fontTools.misc.symfont import x, y, printGreenPen
diff --git a/Lib/fontTools/pens/qtPen.py b/Lib/fontTools/pens/qtPen.py
index 34736453..d08a344f 100644
--- a/Lib/fontTools/pens/qtPen.py
+++ b/Lib/fontTools/pens/qtPen.py
@@ -20,10 +20,10 @@ class QtPen(BasePen):
self.path.lineTo(*p)
def _curveToOne(self, p1, p2, p3):
- self.path.cubicTo(*p1+p2+p3)
+ self.path.cubicTo(*p1, *p2, *p3)
def _qCurveToOne(self, p1, p2):
- self.path.quadTo(*p1+p2)
+ self.path.quadTo(*p1, *p2)
def _closePath(self):
self.path.closeSubpath()
diff --git a/Lib/fontTools/pens/statisticsPen.py b/Lib/fontTools/pens/statisticsPen.py
index abd6ff5e..15830672 100644
--- a/Lib/fontTools/pens/statisticsPen.py
+++ b/Lib/fontTools/pens/statisticsPen.py
@@ -61,10 +61,13 @@ class StatisticsPen(MomentsPen):
# Correlation(X,Y) = Covariance(X,Y) / ( stddev(X) * stddev(Y) )
# https://en.wikipedia.org/wiki/Pearson_product-moment_correlation_coefficient
- correlation = covariance / (stddevX * stddevY)
+ if stddevX * stddevY == 0:
+ correlation = float("NaN")
+ else:
+ correlation = covariance / (stddevX * stddevY)
self.correlation = correlation if abs(correlation) > 1e-3 else 0
- slant = covariance / varianceY
+ slant = covariance / varianceY if varianceY != 0 else float("NaN")
self.slant = slant if abs(slant) > 1e-3 else 0
@@ -82,17 +85,16 @@ def _test(glyphset, upem, glyphs):
transformer = TransformPen(pen, Scale(1./upem))
glyph.draw(transformer)
for item in ['area', 'momentX', 'momentY', 'momentXX', 'momentYY', 'momentXY', 'meanX', 'meanY', 'varianceX', 'varianceY', 'stddevX', 'stddevY', 'covariance', 'correlation', 'slant']:
- if item[0] == '_': continue
print ("%s: %g" % (item, getattr(pen, item)))
def main(args):
if not args:
return
filename, glyphs = args[0], args[1:]
- if not glyphs:
- glyphs = ['e', 'o', 'I', 'slash', 'E', 'zero', 'eight', 'minus', 'equal']
from fontTools.ttLib import TTFont
font = TTFont(filename)
+ if not glyphs:
+ glyphs = font.getGlyphOrder()
_test(font.getGlyphSet(), font['head'].unitsPerEm, glyphs)
if __name__ == '__main__':
diff --git a/Lib/fontTools/pens/svgPathPen.py b/Lib/fontTools/pens/svgPathPen.py
index e92737e3..106e33b7 100644
--- a/Lib/fontTools/pens/svgPathPen.py
+++ b/Lib/fontTools/pens/svgPathPen.py
@@ -23,6 +23,18 @@ class SVGPathPen(BasePen):
used to resolve component references in composite glyphs.
ntos: a callable that takes a number and returns a string, to
customize how numbers are formatted (default: str).
+
+ Note:
+ Fonts have a coordinate system where Y grows up, whereas in SVG,
+ Y grows down. As such, rendering path data from this pen in
+ SVG typically results in upside-down glyphs. You can fix this
+ by wrapping the data from this pen in an SVG group element with
+ transform, or wrap this pen in a transform pen. For example:
+
+ spen = svgPathPen.SVGPathPen(glyphset)
+ pen= TransformPen(spen , (1, 0, 0, -1, 0, 0))
+ glyphset[glyphname].draw(pen)
+ print(tpen.getCommands())
"""
def __init__(self, glyphSet, ntos: Callable[[float], str] = str):
BasePen.__init__(self, glyphSet)
@@ -193,7 +205,70 @@ class SVGPathPen(BasePen):
return "".join(self._commands)
+def main(args=None):
+ """Generate per-character SVG from font and text"""
+
+ if args is None:
+ import sys
+ args = sys.argv[1:]
+
+ from fontTools.ttLib import TTFont
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools pens.svgPathPen", description="Generate SVG from text")
+ parser.add_argument(
+ "font", metavar="font.ttf", help="Font file.")
+ parser.add_argument(
+ "text", metavar="text", help="Text string.")
+ parser.add_argument(
+ "--variations", metavar="AXIS=LOC", default='',
+ help="List of space separated locations. A location consist in "
+ "the name of a variation axis, followed by '=' and a number. E.g.: "
+ "wght=700 wdth=80. The default is the location of the base master.")
+
+ options = parser.parse_args(args)
+
+ font = TTFont(options.font)
+ text = options.text
+
+ location = {}
+ for tag_v in options.variations.split():
+ fields = tag_v.split('=')
+ tag = fields[0].strip()
+ v = int(fields[1])
+ location[tag] = v
+
+ hhea = font['hhea']
+ ascent, descent = hhea.ascent, hhea.descent
+
+ glyphset = font.getGlyphSet(location=location)
+ cmap = font['cmap'].getBestCmap()
+
+ s = ''
+ width = 0
+ for u in text:
+ g = cmap[ord(u)]
+ glyph = glyphset[g]
+
+ pen = SVGPathPen(glyphset)
+ glyph.draw(pen)
+ commands = pen.getCommands()
+
+ s += '<g transform="translate(%d %d) scale(1 -1)"><path d="%s"/></g>\n' % (width, ascent, commands)
+
+ width += glyph.width
+
+ print('<?xml version="1.0" encoding="UTF-8"?>')
+ print('<svg width="%d" height="%d" xmlns="http://www.w3.org/2000/svg">' % (width, ascent-descent))
+ print(s, end='')
+ print('</svg>')
+
+
if __name__ == "__main__":
import sys
- import doctest
- sys.exit(doctest.testmod().failed)
+ if len(sys.argv) == 1:
+ import doctest
+ sys.exit(doctest.testmod().failed)
+
+ sys.exit(main())
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 56d9c0ef..b58e6162 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -10,9 +10,11 @@ from fontTools.ttLib.tables.otBase import USE_HARFBUZZ_REPACKER
from fontTools.otlLib.maxContextCalc import maxCtxFont
from fontTools.pens.basePen import NullPen
from fontTools.misc.loggingTools import Timer
+from fontTools.misc.cliTools import makeOutputFileName
from fontTools.subset.util import _add_method, _uniq_sort
from fontTools.subset.cff import *
from fontTools.subset.svg import *
+from fontTools.varLib import varStore # for subset_varidxes
import sys
import struct
import array
@@ -636,10 +638,16 @@ def prune_post_subset(self, font, options):
self.Value.prune_hints()
self.ValueFormat = self.Value.getEffectiveFormat()
elif self.Format == 2:
- if not options.hinting:
- for v in self.Value:
- v.prune_hints()
- self.ValueFormat = reduce(int.__or__, [v.getEffectiveFormat() for v in self.Value], 0)
+ if None in self.Value:
+ assert self.ValueFormat == 0
+ assert all(v is None for v in self.Value)
+ else:
+ if not options.hinting:
+ for v in self.Value:
+ v.prune_hints()
+ self.ValueFormat = reduce(
+ int.__or__, [v.getEffectiveFormat() for v in self.Value], 0
+ )
# Downgrade to Format 1 if all ValueRecords are the same
if self.Format == 2 and all(v == self.Value[0] for v in self.Value):
@@ -2608,6 +2616,9 @@ class Options(object):
'vertical': ['valt', 'vert', 'vkrn', 'vpal', 'vrt2'],
'ltr': ['ltra', 'ltrm'],
'rtl': ['rtla', 'rtlm'],
+ 'rand': ['rand'],
+ 'justify': ['jalt'],
+ 'private': ['Harf', 'HARF', 'Buzz', 'BUZZ'],
# Complex shapers
'arabic': ['init', 'medi', 'fina', 'isol', 'med2', 'fin2', 'fin3',
'cswh', 'mset', 'stch'],
@@ -3180,12 +3191,7 @@ def main(args=None):
font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames)
if outfile is None:
- basename, _ = splitext(fontfile)
- if options.flavor is not None:
- ext = "." + options.flavor.lower()
- else:
- ext = ".ttf" if font.sfntVersion == "\0\1\0\0" else ".otf"
- outfile = basename + ".subset" + ext
+ outfile = makeOutputFileName(fontfile, overWrite=True, suffix=".subset")
with timer("compile glyph list"):
if wildcard_glyphs:
diff --git a/Lib/fontTools/subset/cff.py b/Lib/fontTools/subset/cff.py
index 0dcb7975..d6872f39 100644
--- a/Lib/fontTools/subset/cff.py
+++ b/Lib/fontTools/subset/cff.py
@@ -3,7 +3,6 @@ from fontTools import ttLib
from fontTools.pens.basePen import NullPen
from fontTools.misc.roundTools import otRound
from fontTools.misc.loggingTools import deprecateFunction
-from fontTools.varLib.varStore import VarStoreInstancer
from fontTools.subset.util import _add_method, _uniq_sort
@@ -109,15 +108,7 @@ def subset_glyphs(self, s):
del csi.file, csi.offsets
if hasattr(font, "FDSelect"):
sel = font.FDSelect
- # XXX We want to set sel.format to None, such that the
- # most compact format is selected. However, OTS was
- # broken and couldn't parse a FDSelect format 0 that
- # happened before CharStrings. As such, always force
- # format 3 until we fix cffLib to always generate
- # FDSelect after CharStrings.
- # https://github.com/khaledhosny/ots/pull/31
- #sel.format = None
- sel.format = 3
+ sel.format = None
sel.gidArray = [sel.gidArray[i] for i in indices]
newCharStrings = {}
for indicesIdx, charsetIdx in enumerate(indices):
diff --git a/Lib/fontTools/subset/svg.py b/Lib/fontTools/subset/svg.py
index e25fb3e6..4ed2cbd2 100644
--- a/Lib/fontTools/subset/svg.py
+++ b/Lib/fontTools/subset/svg.py
@@ -7,13 +7,14 @@ from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple
try:
from lxml import etree
-except ModuleNotFoundError:
+except ImportError:
# lxml is required for subsetting SVG, but we prefer to delay the import error
# until subset_glyphs() is called (i.e. if font to subset has an 'SVG ' table)
etree = None
from fontTools import ttLib
from fontTools.subset.util import _add_method
+from fontTools.ttLib.tables.S_V_G_ import SVGDocument
__all__ = ["subset_glyphs"]
@@ -192,7 +193,7 @@ def ranges(ints: Iterable[int]) -> Iterator[Tuple[int, int]]:
@_add_method(ttLib.getTableClass("SVG "))
def subset_glyphs(self, s) -> bool:
if etree is None:
- raise ModuleNotFoundError("No module named 'lxml', required to subset SVG")
+ raise ImportError("No module named 'lxml', required to subset SVG")
# glyph names (before subsetting)
glyph_order: List[str] = s.orig_glyph_order
@@ -201,10 +202,12 @@ def subset_glyphs(self, s) -> bool:
# map from original to new glyph indices (after subsetting)
glyph_index_map: Dict[int, int] = s.glyph_index_map
- new_docs: List[Tuple[bytes, int, int]] = []
- for doc, start, end in self.docList:
+ new_docs: List[SVGDocument] = []
+ for doc in self.docList:
- glyphs = {glyph_order[i] for i in range(start, end + 1)}.intersection(s.glyphs)
+ glyphs = {
+ glyph_order[i] for i in range(doc.startGlyphID, doc.endGlyphID + 1)
+ }.intersection(s.glyphs)
if not glyphs:
# no intersection: we can drop the whole record
continue
@@ -212,7 +215,7 @@ def subset_glyphs(self, s) -> bool:
svg = etree.fromstring(
# encode because fromstring dislikes xml encoding decl if input is str.
# SVG xml encoding must be utf-8 as per OT spec.
- doc.encode("utf-8"),
+ doc.data.encode("utf-8"),
parser=etree.XMLParser(
# Disable libxml2 security restrictions to support very deep trees.
# Without this we would get an error like this:
@@ -241,7 +244,7 @@ def subset_glyphs(self, s) -> bool:
new_gids = (glyph_index_map[i] for i in gids)
for start, end in ranges(new_gids):
- new_docs.append((new_doc, start, end))
+ new_docs.append(SVGDocument(new_doc, start, end, doc.compressed))
self.docList = new_docs
diff --git a/Lib/fontTools/svgLib/path/parser.py b/Lib/fontTools/svgLib/path/parser.py
index 1fcf8998..e594b2b8 100644
--- a/Lib/fontTools/svgLib/path/parser.py
+++ b/Lib/fontTools/svgLib/path/parser.py
@@ -16,10 +16,13 @@ ARC_COMMANDS = set("Aa")
UPPERCASE = set('MZLHVCSQTA')
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
+
+# https://www.w3.org/TR/css-syntax-3/#number-token-diagram
+# but -6.e-5 will be tokenized as "-6" then "-5" and confuse parsing
FLOAT_RE = re.compile(
r"[-+]?" # optional sign
r"(?:"
- r"(?:0|[1-9][0-9]*)(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)?" # int/float
+ r"(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][-+]?[0-9]+)?" # int/float
r"|"
r"(?:\.[0-9]+(?:[eE][-+]?[0-9]+)?)" # float with leading dot (e.g. '.42')
r")"
@@ -278,8 +281,8 @@ def parse_path(pathdef, pen, current_pos=(0, 0), arc_class=EllipticalArc):
last_control = control
elif command == 'A':
- rx = float(elements.pop())
- ry = float(elements.pop())
+ rx = abs(float(elements.pop()))
+ ry = abs(float(elements.pop()))
rotation = float(elements.pop())
arc_large = bool(int(elements.pop()))
arc_sweep = bool(int(elements.pop()))
diff --git a/Lib/fontTools/ttLib/scaleUpem.py b/Lib/fontTools/ttLib/scaleUpem.py
new file mode 100644
index 00000000..9e0e0ade
--- /dev/null
+++ b/Lib/fontTools/ttLib/scaleUpem.py
@@ -0,0 +1,336 @@
+"""Change the units-per-EM of a font.
+
+AAT and Graphite tables are not supported. CFF/CFF2 fonts
+are de-subroutinized."""
+
+
+from fontTools.ttLib.ttVisitor import TTVisitor
+import fontTools.ttLib as ttLib
+import fontTools.ttLib.tables.otBase as otBase
+import fontTools.ttLib.tables.otTables as otTables
+from fontTools.cffLib import VarStoreData
+import fontTools.cffLib.specializer as cffSpecializer
+from fontTools.misc.fixedTools import otRound
+
+
+__all__ = ["scale_upem", "ScalerVisitor"]
+
+
+class ScalerVisitor(TTVisitor):
+ def __init__(self, scaleFactor):
+ self.scaleFactor = scaleFactor
+
+ def scale(self, v):
+ return otRound(v * self.scaleFactor)
+
+
+@ScalerVisitor.register_attrs(
+ (
+ (ttLib.getTableClass("head"), ("unitsPerEm", "xMin", "yMin", "xMax", "yMax")),
+ (ttLib.getTableClass("post"), ("underlinePosition", "underlineThickness")),
+ (ttLib.getTableClass("VORG"), ("defaultVertOriginY")),
+ (
+ ttLib.getTableClass("hhea"),
+ (
+ "ascent",
+ "descent",
+ "lineGap",
+ "advanceWidthMax",
+ "minLeftSideBearing",
+ "minRightSideBearing",
+ "xMaxExtent",
+ "caretOffset",
+ ),
+ ),
+ (
+ ttLib.getTableClass("vhea"),
+ (
+ "ascent",
+ "descent",
+ "lineGap",
+ "advanceHeightMax",
+ "minTopSideBearing",
+ "minBottomSideBearing",
+ "yMaxExtent",
+ "caretOffset",
+ ),
+ ),
+ (
+ ttLib.getTableClass("OS/2"),
+ (
+ "xAvgCharWidth",
+ "ySubscriptXSize",
+ "ySubscriptYSize",
+ "ySubscriptXOffset",
+ "ySubscriptYOffset",
+ "ySuperscriptXSize",
+ "ySuperscriptYSize",
+ "ySuperscriptXOffset",
+ "ySuperscriptYOffset",
+ "yStrikeoutSize",
+ "yStrikeoutPosition",
+ "sTypoAscender",
+ "sTypoDescender",
+ "sTypoLineGap",
+ "usWinAscent",
+ "usWinDescent",
+ "sxHeight",
+ "sCapHeight",
+ ),
+ ),
+ (
+ otTables.ValueRecord,
+ ("XAdvance", "YAdvance", "XPlacement", "YPlacement"),
+ ), # GPOS
+ (otTables.Anchor, ("XCoordinate", "YCoordinate")), # GPOS
+ (otTables.CaretValue, ("Coordinate")), # GDEF
+ (otTables.BaseCoord, ("Coordinate")), # BASE
+ (otTables.MathValueRecord, ("Value")), # MATH
+ (otTables.ClipBox, ("xMin", "yMin", "xMax", "yMax")), # COLR
+ )
+)
+def visit(visitor, obj, attr, value):
+ setattr(obj, attr, visitor.scale(value))
+
+
+@ScalerVisitor.register_attr(
+ (ttLib.getTableClass("hmtx"), ttLib.getTableClass("vmtx")), "metrics"
+)
+def visit(visitor, obj, attr, metrics):
+ for g in metrics:
+ advance, lsb = metrics[g]
+ metrics[g] = visitor.scale(advance), visitor.scale(lsb)
+
+
+@ScalerVisitor.register_attr(ttLib.getTableClass("VMTX"), "VOriginRecords")
+def visit(visitor, obj, attr, VOriginRecords):
+ for g in VOriginRecords:
+ VOriginRecords[g] = visitor.scale(VOriginRecords[g])
+
+
+@ScalerVisitor.register_attr(ttLib.getTableClass("glyf"), "glyphs")
+def visit(visitor, obj, attr, glyphs):
+ for g in glyphs.values():
+ if g.isComposite():
+ for component in g.components:
+ component.x = visitor.scale(component.x)
+ component.y = visitor.scale(component.y)
+ else:
+ for attr in ("xMin", "xMax", "yMin", "yMax"):
+ v = getattr(g, attr, None)
+ if v is not None:
+ setattr(g, attr, visitor.scale(v))
+
+ glyf = visitor.font["glyf"]
+ coordinates = g.getCoordinates(glyf)[0]
+ for i, (x, y) in enumerate(coordinates):
+ coordinates[i] = visitor.scale(x), visitor.scale(y)
+
+
+@ScalerVisitor.register_attr(ttLib.getTableClass("gvar"), "variations")
+def visit(visitor, obj, attr, variations):
+ for varlist in variations.values():
+ for var in varlist:
+ coordinates = var.coordinates
+ for i, xy in enumerate(coordinates):
+ if xy is None:
+ continue
+ coordinates[i] = visitor.scale(xy[0]), visitor.scale(xy[1])
+
+
+@ScalerVisitor.register_attr(ttLib.getTableClass("kern"), "kernTables")
+def visit(visitor, obj, attr, kernTables):
+ for table in kernTables:
+ kernTable = table.kernTable
+ for k in kernTable.keys():
+ kernTable[k] = visitor.scale(kernTable[k])
+
+
+def _cff_scale(visitor, args):
+ for i, arg in enumerate(args):
+ if not isinstance(arg, list):
+ args[i] = visitor.scale(arg)
+ else:
+ num_blends = arg[-1]
+ _cff_scale(visitor, arg)
+ arg[-1] = num_blends
+
+
+@ScalerVisitor.register_attr(
+ (ttLib.getTableClass("CFF "), ttLib.getTableClass("CFF2")), "cff"
+)
+def visit(visitor, obj, attr, cff):
+ cff.desubroutinize()
+ topDict = cff.topDictIndex[0]
+ varStore = getattr(topDict, "VarStore", None)
+ getNumRegions = varStore.getNumRegions if varStore is not None else None
+ privates = set()
+ for fontname in cff.keys():
+ font = cff[fontname]
+ cs = font.CharStrings
+ for g in font.charset:
+ c, _ = cs.getItemAndSelector(g)
+ privates.add(c.private)
+
+ commands = cffSpecializer.programToCommands(
+ c.program, getNumRegions=getNumRegions
+ )
+ for op, args in commands:
+ _cff_scale(visitor, args)
+ c.program[:] = cffSpecializer.commandsToProgram(commands)
+
+ # Annoying business of scaling numbers that do not matter whatsoever
+
+ for attr in (
+ "UnderlinePosition",
+ "UnderlineThickness",
+ "FontBBox",
+ "StrokeWidth",
+ ):
+ value = getattr(topDict, attr, None)
+ if value is None:
+ continue
+ if isinstance(value, list):
+ _cff_scale(visitor, value)
+ else:
+ setattr(topDict, attr, visitor.scale(value))
+
+ for i in range(6):
+ topDict.FontMatrix[i] /= visitor.scaleFactor
+
+ for private in privates:
+ for attr in (
+ "BlueValues",
+ "OtherBlues",
+ "FamilyBlues",
+ "FamilyOtherBlues",
+ # "BlueScale",
+ # "BlueShift",
+ # "BlueFuzz",
+ "StdHW",
+ "StdVW",
+ "StemSnapH",
+ "StemSnapV",
+ "defaultWidthX",
+ "nominalWidthX",
+ ):
+ value = getattr(private, attr, None)
+ if value is None:
+ continue
+ if isinstance(value, list):
+ _cff_scale(visitor, value)
+ else:
+ setattr(private, attr, visitor.scale(value))
+
+
+# ItemVariationStore
+
+
+@ScalerVisitor.register(otTables.VarData)
+def visit(visitor, varData):
+ for item in varData.Item:
+ for i, v in enumerate(item):
+ item[i] = visitor.scale(v)
+
+
+# COLRv1
+
+
+def _setup_scale_paint(paint, scale):
+ if -2 <= scale <= 2 - (1 >> 14):
+ paint.Format = otTables.PaintFormat.PaintScaleUniform
+ paint.scale = scale
+ return
+
+ transform = otTables.Affine2x3()
+ transform.populateDefaults()
+ transform.xy = transform.yx = transform.dx = transform.dy = 0
+ transform.xx = transform.yy = scale
+
+ paint.Format = otTables.PaintFormat.PaintTransform
+ paint.Transform = transform
+
+
+@ScalerVisitor.register(otTables.BaseGlyphPaintRecord)
+def visit(visitor, record):
+ oldPaint = record.Paint
+
+ scale = otTables.Paint()
+ _setup_scale_paint(scale, visitor.scaleFactor)
+ scale.Paint = oldPaint
+
+ record.Paint = scale
+
+ return True
+
+
+@ScalerVisitor.register(otTables.Paint)
+def visit(visitor, paint):
+ if paint.Format != otTables.PaintFormat.PaintGlyph:
+ return True
+
+ newPaint = otTables.Paint()
+ newPaint.Format = paint.Format
+ newPaint.Paint = paint.Paint
+ newPaint.Glyph = paint.Glyph
+ del paint.Paint
+ del paint.Glyph
+
+ _setup_scale_paint(paint, 1 / visitor.scaleFactor)
+ paint.Paint = newPaint
+
+ visitor.visit(newPaint.Paint)
+
+ return False
+
+
+def scale_upem(font, new_upem):
+ """Change the units-per-EM of font to the new value."""
+ upem = font["head"].unitsPerEm
+ visitor = ScalerVisitor(new_upem / upem)
+ visitor.visit(font)
+
+
+def main(args=None):
+ """Change the units-per-EM of fonts"""
+
+ if args is None:
+ import sys
+
+ args = sys.argv[1:]
+
+ from fontTools.ttLib import TTFont
+ from fontTools.misc.cliTools import makeOutputFileName
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ "fonttools ttLib.scaleUpem", description="Change the units-per-EM of fonts"
+ )
+ parser.add_argument("font", metavar="font", help="Font file.")
+ parser.add_argument(
+ "new_upem", metavar="new-upem", help="New units-per-EM integer value."
+ )
+ parser.add_argument(
+ "--output-file", metavar="path", default=None, help="Output file."
+ )
+
+ options = parser.parse_args(args)
+
+ font = TTFont(options.font)
+ new_upem = int(options.new_upem)
+ output_file = (
+ options.output_file
+ if options.output_file is not None
+ else makeOutputFileName(options.font, overWrite=True, suffix="-scaled")
+ )
+
+ scale_upem(font, new_upem)
+
+ print("Writing %s" % output_file)
+ font.save(output_file)
+
+
+if __name__ == "__main__":
+ import sys
+
+ sys.exit(main())
diff --git a/Lib/fontTools/ttLib/tables/E_B_D_T_.py b/Lib/fontTools/ttLib/tables/E_B_D_T_.py
index 0bd2ab99..ae716512 100644
--- a/Lib/fontTools/ttLib/tables/E_B_D_T_.py
+++ b/Lib/fontTools/ttLib/tables/E_B_D_T_.py
@@ -398,12 +398,17 @@ class BitmapGlyph(object):
# Allow lazy decompile.
if attr[:2] == '__':
raise AttributeError(attr)
- if not hasattr(self, "data"):
+ if attr == "data":
raise AttributeError(attr)
self.decompile()
del self.data
return getattr(self, attr)
+ def ensureDecompiled(self, recurse=False):
+ if hasattr(self, "data"):
+ self.decompile()
+ del self.data
+
# Not a fan of this but it is needed for safer safety checking.
def getFormat(self):
return safeEval(self.__class__.__name__[len(_bitmapGlyphSubclassPrefix):])
diff --git a/Lib/fontTools/ttLib/tables/E_B_L_C_.py b/Lib/fontTools/ttLib/tables/E_B_L_C_.py
index cfdbca7b..bb3d2140 100644
--- a/Lib/fontTools/ttLib/tables/E_B_L_C_.py
+++ b/Lib/fontTools/ttLib/tables/E_B_L_C_.py
@@ -338,11 +338,15 @@ class EblcIndexSubTable(object):
# Allow lazy decompile.
if attr[:2] == '__':
raise AttributeError(attr)
- if not hasattr(self, "data"):
+ if attr == "data":
raise AttributeError(attr)
self.decompile()
return getattr(self, attr)
+ def ensureDecompiled(self, recurse=False):
+ if hasattr(self, "data"):
+ self.decompile()
+
# This method just takes care of the indexSubHeader. Implementing subclasses
# should call it to compile the indexSubHeader and then continue compiling
# the remainder of their unique format.
diff --git a/Lib/fontTools/ttLib/tables/S_V_G_.py b/Lib/fontTools/ttLib/tables/S_V_G_.py
index bc0e533d..49e98d03 100644
--- a/Lib/fontTools/ttLib/tables/S_V_G_.py
+++ b/Lib/fontTools/ttLib/tables/S_V_G_.py
@@ -17,9 +17,11 @@ The XML format is:
</SVG>
"""
-from fontTools.misc.textTools import bytesjoin, strjoin, tobytes, tostr
+from fontTools.misc.textTools import bytesjoin, safeEval, strjoin, tobytes, tostr
from fontTools.misc import sstruct
from . import DefaultTable
+from collections.abc import Sequence
+from dataclasses import dataclass, astuple
from io import BytesIO
import struct
import logging
@@ -75,15 +77,18 @@ class table_S_V_G_(DefaultTable.DefaultTable):
start = entry.svgDocOffset + subTableStart
end = start + entry.svgDocLength
doc = data[start:end]
+ compressed = False
if doc.startswith(b"\x1f\x8b"):
import gzip
bytesIO = BytesIO(doc)
with gzip.GzipFile(None, "r", fileobj=bytesIO) as gunzipper:
doc = gunzipper.read()
- self.compressed = True
del bytesIO
+ compressed = True
doc = tostr(doc, "utf_8")
- self.docList.append( [doc, entry.startGlyphID, entry.endGlyphID] )
+ self.docList.append(
+ SVGDocument(doc, entry.startGlyphID, entry.endGlyphID, compressed)
+ )
def compile(self, ttFont):
version = 0
@@ -96,12 +101,18 @@ class table_S_V_G_(DefaultTable.DefaultTable):
entryList.append(datum)
curOffset = len(datum) + doc_index_entry_format_0Size*numEntries
seenDocs = {}
- for doc, startGlyphID, endGlyphID in self.docList:
- docBytes = tobytes(doc, encoding="utf_8")
- if getattr(self, "compressed", False) and not docBytes.startswith(b"\x1f\x8b"):
+ allCompressed = getattr(self, "compressed", False)
+ for i, doc in enumerate(self.docList):
+ if isinstance(doc, (list, tuple)):
+ doc = SVGDocument(*doc)
+ self.docList[i] = doc
+ docBytes = tobytes(doc.data, encoding="utf_8")
+ if (allCompressed or doc.compressed) and not docBytes.startswith(b"\x1f\x8b"):
import gzip
bytesIO = BytesIO()
- with gzip.GzipFile(None, "w", fileobj=bytesIO) as gzipper:
+ # mtime=0 strips the useless timestamp and makes gzip output reproducible;
+ # equivalent to `gzip -n`
+ with gzip.GzipFile(None, "w", fileobj=bytesIO, mtime=0) as gzipper:
gzipper.write(docBytes)
gzipped = bytesIO.getvalue()
if len(gzipped) < len(docBytes):
@@ -115,7 +126,7 @@ class table_S_V_G_(DefaultTable.DefaultTable):
curOffset += docLength
seenDocs[docBytes] = docOffset
docList.append(docBytes)
- entry = struct.pack(">HHLL", startGlyphID, endGlyphID, docOffset, docLength)
+ entry = struct.pack(">HHLL", doc.startGlyphID, doc.endGlyphID, docOffset, docLength)
entryList.append(entry)
entryList.extend(docList)
svgDocData = bytesjoin(entryList)
@@ -127,10 +138,16 @@ class table_S_V_G_(DefaultTable.DefaultTable):
return data
def toXML(self, writer, ttFont):
- for doc, startGID, endGID in self.docList:
- writer.begintag("svgDoc", startGlyphID=startGID, endGlyphID=endGID)
+ for i, doc in enumerate(self.docList):
+ if isinstance(doc, (list, tuple)):
+ doc = SVGDocument(*doc)
+ self.docList[i] = doc
+ attrs = {"startGlyphID": doc.startGlyphID, "endGlyphID": doc.endGlyphID}
+ if doc.compressed:
+ attrs["compressed"] = 1
+ writer.begintag("svgDoc", **attrs)
writer.newline()
- writer.writecdata(doc)
+ writer.writecdata(doc.data)
writer.newline()
writer.endtag("svgDoc")
writer.newline()
@@ -143,7 +160,8 @@ class table_S_V_G_(DefaultTable.DefaultTable):
doc = doc.strip()
startGID = int(attrs["startGlyphID"])
endGID = int(attrs["endGlyphID"])
- self.docList.append( [doc, startGID, endGID] )
+ compressed = bool(safeEval(attrs.get("compressed", "0")))
+ self.docList.append(SVGDocument(doc, startGID, endGID, compressed))
else:
log.warning("Unknown %s %s", name, content)
@@ -157,3 +175,23 @@ class DocumentIndexEntry(object):
def __repr__(self):
return "startGlyphID: %s, endGlyphID: %s, svgDocOffset: %s, svgDocLength: %s" % (self.startGlyphID, self.endGlyphID, self.svgDocOffset, self.svgDocLength)
+
+
+@dataclass
+class SVGDocument(Sequence):
+ data: str
+ startGlyphID: int
+ endGlyphID: int
+ compressed: bool = False
+
+ # Previously, the SVG table's docList attribute contained a lists of 3 items:
+ # [doc, startGlyphID, endGlyphID]; later, we added a `compressed` attribute.
+ # For backward compatibility with code that depends of them being sequences of
+ # fixed length=3, we subclass the Sequence abstract base class and pretend only
+ # the first three items are present. 'compressed' is only accessible via named
+ # attribute lookup like regular dataclasses: i.e. `doc.compressed`, not `doc[3]`
+ def __getitem__(self, index):
+ return astuple(self)[:3][index]
+
+ def __len__(self):
+ return 3
diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
index 9bd59a6b..ef2b5758 100644
--- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
@@ -164,7 +164,9 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
if ttFont.lazy is False: # Be lazy for None and True
self.ensureDecompiled()
- def ensureDecompiled(self):
+ def ensureDecompiled(self, recurse=False):
+ # The recurse argument is unused, but part of the signature of
+ # ensureDecompiled across the library.
for st in self.tables:
st.ensureDecompiled()
@@ -241,7 +243,9 @@ class CmapSubtable(object):
self.platEncID = None #: The encoding ID of this subtable (interpretation depends on ``platformID``)
self.language = None #: The language ID of this subtable (Macintosh platform only)
- def ensureDecompiled(self):
+ def ensureDecompiled(self, recurse=False):
+ # The recurse argument is unused, but part of the signature of
+ # ensureDecompiled across the library.
if self.data is None:
return
self.decompile(None, None) # use saved data.
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
index 14c4519d..745ef72b 100644
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -112,7 +112,9 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
if ttFont.lazy is False: # Be lazy for None and True
self.ensureDecompiled()
- def ensureDecompiled(self):
+ def ensureDecompiled(self, recurse=False):
+ # The recurse argument is unused, but part of the signature of
+ # ensureDecompiled across the library.
for glyph in self.glyphs.values():
glyph.expand(self)
diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
index bc283cfe..dd198f4b 100644
--- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
@@ -1,3 +1,4 @@
+from functools import partial
from fontTools.misc import sstruct
from fontTools.misc.textTools import safeEval
from . import DefaultTable
@@ -36,6 +37,46 @@ GVAR_HEADER_FORMAT = """
GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
+class _lazy_dict(dict):
+
+ def get(self, k, *args):
+ v = super().get(k, *args)
+ if callable(v):
+ v = v()
+ self[k] = v
+ return v
+
+ def __getitem__(self, k):
+ v = super().__getitem__(k)
+ if callable(v):
+ v = v()
+ self[k] = v
+ return v
+
+ def items(self):
+ if not hasattr(self, '_loaded'):
+ self._load()
+ return super().items()
+
+ def values(self):
+ if not hasattr(self, '_loaded'):
+ self._load()
+ return super().values()
+
+ def __eq__(self, other):
+ if not hasattr(self, '_loaded'):
+ self._load()
+ return super().__eq__(other)
+
+ def __neq__(self, other):
+ if not hasattr(self, '_loaded'):
+ self._load()
+ return super().__neq__(other)
+
+ def _load(self):
+ for k in self:
+ self[k]
+ self._loaded = True
class table__g_v_a_r(DefaultTable.DefaultTable):
dependencies = ["fvar", "glyf"]
@@ -97,23 +138,19 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
offsets = self.decompileOffsets_(data[GVAR_HEADER_SIZE:], tableFormat=(self.flags & 1), glyphCount=self.glyphCount)
sharedCoords = tv.decompileSharedTuples(
axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples)
- self.variations = {}
+ self.variations = _lazy_dict()
offsetToData = self.offsetToGlyphVariationData
glyf = ttFont['glyf']
- for i in range(self.glyphCount):
- glyphName = glyphs[i]
+
+ def decompileVarGlyph(glyphName, gid):
glyph = glyf[glyphName]
numPointsInGlyph = self.getNumPoints_(glyph)
- gvarData = data[offsetToData + offsets[i] : offsetToData + offsets[i + 1]]
- try:
- self.variations[glyphName] = decompileGlyph_(
- numPointsInGlyph, sharedCoords, axisTags, gvarData)
- except Exception:
- log.error(
- "Failed to decompile deltas for glyph '%s' (%d points)",
- glyphName, numPointsInGlyph,
- )
- raise
+ gvarData = data[offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]]
+ return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
+
+ for gid in range(self.glyphCount):
+ glyphName = glyphs[gid]
+ self.variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
@staticmethod
def decompileOffsets_(data, tableFormat, glyphCount):
diff --git a/Lib/fontTools/ttLib/tables/_k_e_r_n.py b/Lib/fontTools/ttLib/tables/_k_e_r_n.py
index f3f714b2..bcad2cea 100644
--- a/Lib/fontTools/ttLib/tables/_k_e_r_n.py
+++ b/Lib/fontTools/ttLib/tables/_k_e_r_n.py
@@ -161,9 +161,11 @@ class KernTable_format_0(object):
len(data) - 6 * nPairs)
def compile(self, ttFont):
- nPairs = len(self.kernTable)
+ nPairs = min(len(self.kernTable), 0xFFFF)
searchRange, entrySelector, rangeShift = getSearchRange(nPairs, 6)
searchRange &= 0xFFFF
+ entrySelector = min(entrySelector, 0xFFFF)
+ rangeShift = min(rangeShift, 0xFFFF)
data = struct.pack(
">HHHH", nPairs, searchRange, entrySelector, rangeShift)
diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py
index d30892f3..1bd3198d 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -1,18 +1,23 @@
from fontTools.config import OPTIONS
from fontTools.misc.textTools import Tag, bytesjoin
from .DefaultTable import DefaultTable
+from enum import IntEnum
import sys
import array
import struct
import logging
-from typing import Iterator, NamedTuple, Optional
+from functools import lru_cache
+from typing import Iterator, NamedTuple, Optional, Tuple
log = logging.getLogger(__name__)
have_uharfbuzz = False
try:
import uharfbuzz as hb
- have_uharfbuzz = True
+ # repack method added in uharfbuzz >= 0.23; if uharfbuzz *can* be
+ # imported but repack method is missing, behave as if uharfbuzz
+ # is not available (fallback to the slower Python implementation)
+ have_uharfbuzz = callable(getattr(hb, "repack", None))
except ImportError:
pass
@@ -36,6 +41,25 @@ class OTLOffsetOverflowError(Exception):
def __str__(self):
return repr(self.value)
+class RepackerState(IntEnum):
+ # Repacking control flow is implemnted using a state machine. The state machine table:
+ #
+ # State | Packing Success | Packing Failed | Exception Raised |
+ # ------------+-----------------+----------------+------------------+
+ # PURE_FT | Return result | PURE_FT | Return failure |
+ # HB_FT | Return result | HB_FT | FT_FALLBACK |
+ # FT_FALLBACK | HB_FT | FT_FALLBACK | Return failure |
+
+ # Pack only with fontTools, don't allow sharing between extensions.
+ PURE_FT = 1
+
+ # Attempt to pack with harfbuzz (allowing sharing between extensions)
+ # use fontTools to attempt overflow resolution.
+ HB_FT = 2
+
+ # Fallback if HB/FT packing gets stuck. Pack only with fontTools, don't allow sharing between
+ # extensions.
+ FT_FALLBACK = 3
class BaseTTXConverter(DefaultTable):
@@ -96,62 +120,98 @@ class BaseTTXConverter(DefaultTable):
self.tableTag,
)
+ if (use_hb_repack in (None, True)
+ and have_uharfbuzz
+ and self.tableTag in ("GSUB", "GPOS")):
+ state = RepackerState.HB_FT
+ else:
+ state = RepackerState.PURE_FT
+
hb_first_error_logged = False
+ lastOverflowRecord = None
while True:
try:
writer = OTTableWriter(tableTag=self.tableTag)
self.table.compile(writer, font)
- if (
- use_hb_repack in (None, True)
- and have_uharfbuzz
- and self.tableTag in ("GSUB", "GPOS")
- ):
- try:
- log.debug("serializing '%s' with hb.repack", self.tableTag)
- return writer.getAllDataUsingHarfbuzz()
- except (ValueError, MemoryError, hb.RepackerError) as e:
- # Only log hb repacker errors the first time they occur in
- # the offset-overflow resolution loop, they are just noisy.
- # Maybe we can revisit this if/when uharfbuzz actually gives
- # us more info as to why hb.repack failed...
- if not hb_first_error_logged:
- error_msg = f"{type(e).__name__}"
- if str(e) != "":
- error_msg += f": {e}"
- log.warning(
- "hb.repack failed to serialize '%s', reverting to "
- "pure-python serializer; the error message was: %s",
- self.tableTag,
- error_msg,
- )
- hb_first_error_logged = True
- return writer.getAllData(remove_duplicate=False)
- return writer.getAllData()
+ if state == RepackerState.HB_FT:
+ return self.tryPackingHarfbuzz(writer, hb_first_error_logged)
+ elif state == RepackerState.PURE_FT:
+ return self.tryPackingFontTools(writer)
+ elif state == RepackerState.FT_FALLBACK:
+ # Run packing with FontTools only, but don't return the result as it will
+ # not be optimally packed. Once a successful packing has been found, state is
+ # changed back to harfbuzz packing to produce the final, optimal, packing.
+ self.tryPackingFontTools(writer)
+ log.debug("Re-enabling sharing between extensions and switching back to "
+ "harfbuzz+fontTools packing.")
+ state = RepackerState.HB_FT
except OTLOffsetOverflowError as e:
+ hb_first_error_logged = True
+ ok = self.tryResolveOverflow(font, e, lastOverflowRecord)
+ lastOverflowRecord = e.value
- if overflowRecord == e.value:
- raise # Oh well...
-
- overflowRecord = e.value
- log.info("Attempting to fix OTLOffsetOverflowError %s", e)
- lastItem = overflowRecord
+ if ok:
+ continue
- ok = 0
- if overflowRecord.itemName is None:
- from .otTables import fixLookupOverFlows
- ok = fixLookupOverFlows(font, overflowRecord)
+ if state is RepackerState.HB_FT:
+ log.debug("Harfbuzz packing out of resolutions, disabling sharing between extensions and "
+ "switching to fontTools only packing.")
+ state = RepackerState.FT_FALLBACK
else:
- from .otTables import fixSubTableOverFlows
- ok = fixSubTableOverFlows(font, overflowRecord)
- if not ok:
- # Try upgrading lookup to Extension and hope
- # that cross-lookup sharing not happening would
- # fix overflow...
- from .otTables import fixLookupOverFlows
- ok = fixLookupOverFlows(font, overflowRecord)
- if not ok:
- raise
+ raise
+
+ def tryPackingHarfbuzz(self, writer, hb_first_error_logged):
+ try:
+ log.debug("serializing '%s' with hb.repack", self.tableTag)
+ return writer.getAllDataUsingHarfbuzz(self.tableTag)
+ except (ValueError, MemoryError, hb.RepackerError) as e:
+ # Only log hb repacker errors the first time they occur in
+ # the offset-overflow resolution loop, they are just noisy.
+ # Maybe we can revisit this if/when uharfbuzz actually gives
+ # us more info as to why hb.repack failed...
+ if not hb_first_error_logged:
+ error_msg = f"{type(e).__name__}"
+ if str(e) != "":
+ error_msg += f": {e}"
+ log.warning(
+ "hb.repack failed to serialize '%s', attempting fonttools resolutions "
+ "; the error message was: %s",
+ self.tableTag,
+ error_msg,
+ )
+ hb_first_error_logged = True
+ return writer.getAllData(remove_duplicate=False)
+
+
+ def tryPackingFontTools(self, writer):
+ return writer.getAllData()
+
+
+ def tryResolveOverflow(self, font, e, lastOverflowRecord):
+ ok = 0
+ if lastOverflowRecord == e.value:
+ # Oh well...
+ return ok
+
+ overflowRecord = e.value
+ log.info("Attempting to fix OTLOffsetOverflowError %s", e)
+
+ if overflowRecord.itemName is None:
+ from .otTables import fixLookupOverFlows
+ ok = fixLookupOverFlows(font, overflowRecord)
+ else:
+ from .otTables import fixSubTableOverFlows
+ ok = fixSubTableOverFlows(font, overflowRecord)
+
+ if ok:
+ return ok
+
+ # Try upgrading lookup to Extension and hope
+ # that cross-lookup sharing not happening would
+ # fix overflow...
+ from .otTables import fixLookupOverFlows
+ return fixLookupOverFlows(font, overflowRecord)
def toXML(self, writer, font):
self.table.toXML2(writer, font)
@@ -164,8 +224,8 @@ class BaseTTXConverter(DefaultTable):
self.table.fromXML(name, attrs, content, font)
self.table.populateDefaults()
- def ensureDecompiled(self):
- self.table.ensureDecompiled(recurse=True)
+ def ensureDecompiled(self, recurse=True):
+ self.table.ensureDecompiled(recurse=recurse)
# https://github.com/fonttools/fonttools/pull/2285#issuecomment-834652928
@@ -380,7 +440,7 @@ class OTTableWriter(object):
return NotImplemented
return self.offsetSize == other.offsetSize and self.items == other.items
- def _doneWriting(self, internedTables):
+ def _doneWriting(self, internedTables, shareExtension=False):
# Convert CountData references to data string items
# collapse duplicate table references to a unique entry
# "tables" are OTTableWriter objects.
@@ -396,7 +456,7 @@ class OTTableWriter(object):
# See: https://github.com/fonttools/fonttools/issues/518
dontShare = hasattr(self, 'DontShare')
- if isExtension:
+ if isExtension and not shareExtension:
internedTables = {}
items = self.items
@@ -405,7 +465,7 @@ class OTTableWriter(object):
if hasattr(item, "getCountData"):
items[i] = item.getCountData()
elif hasattr(item, "getData"):
- item._doneWriting(internedTables)
+ item._doneWriting(internedTables, shareExtension=shareExtension)
# At this point, all subwriters are hashable based on their items.
# (See hash and comparison magic methods above.) So the ``setdefault``
# call here will return the first writer object we've seen with
@@ -510,7 +570,7 @@ class OTTableWriter(object):
child_idx = item_idx = item._gatherGraphForHarfbuzz(tables, obj_list, done, item_idx, virtual_edges)
else:
child_idx = done[id(item)]
-
+
real_edge = (pos, item.offsetSize, child_idx)
real_links.append(real_edge)
offset_pos += item.offsetSize
@@ -524,7 +584,7 @@ class OTTableWriter(object):
return item_idx
- def getAllDataUsingHarfbuzz(self):
+ def getAllDataUsingHarfbuzz(self, tableTag):
"""The Whole table is represented as a Graph.
Assemble graph data and call Harfbuzz repacker to pack the table.
Harfbuzz repacker is faster and retain as much sub-table sharing as possible, see also:
@@ -533,7 +593,7 @@ class OTTableWriter(object):
https://github.com/harfbuzz/uharfbuzz/blob/main/src/uharfbuzz/_harfbuzz.pyx#L1149
"""
internedTables = {}
- self._doneWriting(internedTables)
+ self._doneWriting(internedTables, shareExtension=True)
tables = []
obj_list = []
done = {}
@@ -552,7 +612,10 @@ class OTTableWriter(object):
tableData = table.getDataForHarfbuzz()
data.append(tableData)
- return hb.repack(data, obj_list)
+ if hasattr(hb, "repack_with_tag"):
+ return hb.repack_with_tag(str(tableTag), data, obj_list)
+ else:
+ return hb.repack(data, obj_list)
def getAllData(self, remove_duplicate=True):
"""Assemble all data, including all subtables."""
@@ -808,6 +871,9 @@ class BaseTable(object):
#elif not conv.isCount:
# # Warn?
# pass
+ if hasattr(conv, "DEFAULT"):
+ # OptionalValue converters (e.g. VarIndex)
+ setattr(self, conv.name, conv.DEFAULT)
def decompile(self, reader, font):
self.readFormat(reader)
@@ -1042,6 +1108,10 @@ class BaseTable(object):
if isinstance(v, BaseTable)
)
+ # instance (not @class)method for consistency with FormatSwitchingBaseTable
+ def getVariableAttrs(self):
+ return getVariableAttrs(self.__class__)
+
class FormatSwitchingBaseTable(BaseTable):
@@ -1076,6 +1146,9 @@ class FormatSwitchingBaseTable(BaseTable):
def toXML(self, xmlWriter, font, attrs=None, name=None):
BaseTable.toXML(self, xmlWriter, font, attrs, name)
+ def getVariableAttrs(self):
+ return getVariableAttrs(self.__class__, self.Format)
+
class UInt8FormatSwitchingBaseTable(FormatSwitchingBaseTable):
def readFormat(self, reader):
@@ -1097,6 +1170,33 @@ def getFormatSwitchingBaseTableClass(formatType):
raise TypeError(f"Unsupported format type: {formatType!r}")
+# memoize since these are parsed from otData.py, thus stay constant
+@lru_cache()
+def getVariableAttrs(cls: BaseTable, fmt: Optional[int] = None) -> Tuple[str]:
+ """Return sequence of variable table field names (can be empty).
+
+ Attributes are deemed "variable" when their otData.py's description contain
+ 'VarIndexBase + {offset}', e.g. COLRv1 PaintVar* tables.
+ """
+ if not issubclass(cls, BaseTable):
+ raise TypeError(cls)
+ if issubclass(cls, FormatSwitchingBaseTable):
+ if fmt is None:
+ raise TypeError(f"'fmt' is required for format-switching {cls.__name__}")
+ converters = cls.convertersByName[fmt]
+ else:
+ converters = cls.convertersByName
+ # assume if no 'VarIndexBase' field is present, table has no variable fields
+ if "VarIndexBase" not in converters:
+ return ()
+ varAttrs = {}
+ for name, conv in converters.items():
+ offset = conv.getVarIndexOffset()
+ if offset is not None:
+ varAttrs[name] = offset
+ return tuple(sorted(varAttrs, key=varAttrs.__getitem__))
+
+
#
# Support for ValueRecords
#
diff --git a/Lib/fontTools/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py
index 44fcd0ab..b08f1f19 100644
--- a/Lib/fontTools/ttLib/tables/otConverters.py
+++ b/Lib/fontTools/ttLib/tables/otConverters.py
@@ -15,10 +15,13 @@ from .otTables import (lookupTypes, AATStateTable, AATState, AATAction,
ContextualMorphAction, LigatureMorphAction,
InsertionMorphAction, MorxSubtable,
ExtendMode as _ExtendMode,
- CompositeMode as _CompositeMode)
+ CompositeMode as _CompositeMode,
+ NO_VARIATION_INDEX)
from itertools import zip_longest
from functools import partial
+import re
import struct
+from typing import Optional
import logging
@@ -60,7 +63,7 @@ def buildConverters(tableSpec, tableNamespace):
else:
converterClass = eval(tp, tableNamespace, converterMapping)
- conv = converterClass(name, repeat, aux)
+ conv = converterClass(name, repeat, aux, description=descr)
if conv.tableClass:
# A "template" such as OffsetTo(AType) knowss the table class already
@@ -136,7 +139,7 @@ class BaseConverter(object):
"""Base class for converter objects. Apart from the constructor, this
is an abstract class."""
- def __init__(self, name, repeat, aux, tableClass=None):
+ def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
self.name = name
self.repeat = repeat
self.aux = aux
@@ -159,6 +162,7 @@ class BaseConverter(object):
"BaseGlyphRecordCount",
"LayerRecordCount",
]
+ self.description = description
def readArray(self, reader, font, tableDict, count):
"""Read an array of values from the reader."""
@@ -211,6 +215,15 @@ class BaseConverter(object):
"""Write a value to XML."""
raise NotImplementedError(self)
+ varIndexBasePlusOffsetRE = re.compile(r"VarIndexBase\s*\+\s*(\d+)")
+
+ def getVarIndexOffset(self) -> Optional[int]:
+ """If description has `VarIndexBase + {offset}`, return the offset else None."""
+ m = self.varIndexBasePlusOffsetRE.search(self.description)
+ if not m:
+ return None
+ return int(m.group(1))
+
class SimpleValue(BaseConverter):
@staticmethod
@@ -270,7 +283,7 @@ class Flags32(ULong):
return "0x%08X" % value
class VarIndex(OptionalValue, ULong):
- DEFAULT = 0xFFFFFFFF
+ DEFAULT = NO_VARIATION_INDEX
class Short(IntValue):
staticSize = 2
@@ -402,40 +415,51 @@ class DeciPoints(FloatValue):
def write(self, writer, font, tableDict, value, repeatIndex=None):
writer.writeUShort(round(value * 10))
-class Fixed(FloatValue):
- staticSize = 4
+class BaseFixedValue(FloatValue):
+ staticSize = NotImplemented
+ precisionBits = NotImplemented
+ readerMethod = NotImplemented
+ writerMethod = NotImplemented
def read(self, reader, font, tableDict):
- return fi2fl(reader.readLong(), 16)
+ return self.fromInt(getattr(reader, self.readerMethod)())
def write(self, writer, font, tableDict, value, repeatIndex=None):
- writer.writeLong(fl2fi(value, 16))
- @staticmethod
- def fromString(value):
- return str2fl(value, 16)
- @staticmethod
- def toString(value):
- return fl2str(value, 16)
+ getattr(writer, self.writerMethod)(self.toInt(value))
+ @classmethod
+ def fromInt(cls, value):
+ return fi2fl(value, cls.precisionBits)
+ @classmethod
+ def toInt(cls, value):
+ return fl2fi(value, cls.precisionBits)
+ @classmethod
+ def fromString(cls, value):
+ return str2fl(value, cls.precisionBits)
+ @classmethod
+ def toString(cls, value):
+ return fl2str(value, cls.precisionBits)
-class F2Dot14(FloatValue):
+class Fixed(BaseFixedValue):
+ staticSize = 4
+ precisionBits = 16
+ readerMethod = "readLong"
+ writerMethod = "writeLong"
+
+class F2Dot14(BaseFixedValue):
staticSize = 2
- def read(self, reader, font, tableDict):
- return fi2fl(reader.readShort(), 14)
- def write(self, writer, font, tableDict, value, repeatIndex=None):
- writer.writeShort(fl2fi(value, 14))
- @staticmethod
- def fromString(value):
- return str2fl(value, 14)
- @staticmethod
- def toString(value):
- return fl2str(value, 14)
+ precisionBits = 14
+ readerMethod = "readShort"
+ writerMethod = "writeShort"
class Angle(F2Dot14):
# angles are specified in degrees, and encoded as F2Dot14 fractions of half
# circle: e.g. 1.0 => 180, -0.5 => -90, -2.0 => -360, etc.
+ bias = 0.0
factor = 1.0/(1<<14) * 180 # 0.010986328125
- def read(self, reader, font, tableDict):
- return super().read(reader, font, tableDict) * 180
- def write(self, writer, font, tableDict, value, repeatIndex=None):
- super().write(writer, font, tableDict, value / 180, repeatIndex=repeatIndex)
+ @classmethod
+ def fromInt(cls, value):
+ return (super().fromInt(value) + cls.bias) * 180
+ @classmethod
+ def toInt(cls, value):
+ return super().toInt((value / 180) - cls.bias)
@classmethod
def fromString(cls, value):
# quantize to nearest multiples of minimum fixed-precision angle
@@ -444,6 +468,11 @@ class Angle(F2Dot14):
def toString(cls, value):
return nearestMultipleShortestRepr(value, cls.factor)
+class BiasedAngle(Angle):
+ # A bias of 1.0 is used in the representation of start and end angles
+ # of COLRv1 PaintSweepGradients to allow for encoding +360deg
+ bias = 1.0
+
class Version(SimpleValue):
staticSize = 4
def read(self, reader, font, tableDict):
@@ -686,8 +715,10 @@ class FeatureParams(Table):
class ValueFormat(IntValue):
staticSize = 2
- def __init__(self, name, repeat, aux, tableClass=None):
- BaseConverter.__init__(self, name, repeat, aux, tableClass)
+ def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
+ BaseConverter.__init__(
+ self, name, repeat, aux, tableClass, description=description
+ )
self.which = "ValueFormat" + ("2" if name[-1] == "2" else "1")
def read(self, reader, font, tableDict):
format = reader.readUShort()
@@ -720,8 +751,10 @@ class ValueRecord(ValueFormat):
class AATLookup(BaseConverter):
BIN_SEARCH_HEADER_SIZE = 10
- def __init__(self, name, repeat, aux, tableClass):
- BaseConverter.__init__(self, name, repeat, aux, tableClass)
+ def __init__(self, name, repeat, aux, tableClass, *, description=""):
+ BaseConverter.__init__(
+ self, name, repeat, aux, tableClass, description=description
+ )
if issubclass(self.tableClass, SimpleValue):
self.converter = self.tableClass(name='Value', repeat=None, aux=None)
else:
@@ -1019,8 +1052,10 @@ class MorxSubtableConverter(BaseConverter):
val: key for key, val in _PROCESSING_ORDERS.items()
}
- def __init__(self, name, repeat, aux):
- BaseConverter.__init__(self, name, repeat, aux)
+ def __init__(self, name, repeat, aux, tableClass=None, *, description=""):
+ BaseConverter.__init__(
+ self, name, repeat, aux, tableClass, description=description
+ )
def _setTextDirectionFromCoverageFlags(self, flags, subtable):
if (flags & 0x20) != 0:
@@ -1140,8 +1175,10 @@ class MorxSubtableConverter(BaseConverter):
# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6Tables.html#ExtendedStateHeader
# TODO: Untangle the implementation of the various lookup-specific formats.
class STXHeader(BaseConverter):
- def __init__(self, name, repeat, aux, tableClass):
- BaseConverter.__init__(self, name, repeat, aux, tableClass)
+ def __init__(self, name, repeat, aux, tableClass, *, description=""):
+ BaseConverter.__init__(
+ self, name, repeat, aux, tableClass, description=description
+ )
assert issubclass(self.tableClass, AATAction)
self.classLookup = AATLookup("GlyphClasses", None, None, UShort)
if issubclass(self.tableClass, ContextualMorphAction):
@@ -1742,6 +1779,7 @@ converterMapping = {
"Fixed": Fixed,
"F2Dot14": F2Dot14,
"Angle": Angle,
+ "BiasedAngle": BiasedAngle,
"struct": Struct,
"Offset": Table,
"LOffset": LTable,
diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py
index dd4033e4..2e65869f 100755
--- a/Lib/fontTools/ttLib/tables/otData.py
+++ b/Lib/fontTools/ttLib/tables/otData.py
@@ -1623,10 +1623,10 @@ otData = [
('ClipBoxFormat2', [
('uint8', 'Format', None, None, 'Format for variable ClipBox: set to 2.'),
- ('int16', 'xMin', None, None, 'Minimum x of clip box.'),
- ('int16', 'yMin', None, None, 'Minimum y of clip box.'),
- ('int16', 'xMax', None, None, 'Maximum x of clip box.'),
- ('int16', 'yMax', None, None, 'Maximum y of clip box.'),
+ ('int16', 'xMin', None, None, 'Minimum x of clip box. VarIndexBase + 0.'),
+ ('int16', 'yMin', None, None, 'Minimum y of clip box. VarIndexBase + 1.'),
+ ('int16', 'xMax', None, None, 'Maximum x of clip box. VarIndexBase + 2.'),
+ ('int16', 'yMax', None, None, 'Maximum y of clip box. VarIndexBase + 3.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1648,12 +1648,12 @@ otData = [
('Fixed', 'dy', None, None, 'Translation in y direction'),
]),
('VarAffine2x3', [
- ('Fixed', 'xx', None, None, 'x-part of x basis vector'),
- ('Fixed', 'yx', None, None, 'y-part of x basis vector'),
- ('Fixed', 'xy', None, None, 'x-part of y basis vector'),
- ('Fixed', 'yy', None, None, 'y-part of y basis vector'),
- ('Fixed', 'dx', None, None, 'Translation in x direction'),
- ('Fixed', 'dy', None, None, 'Translation in y direction'),
+ ('Fixed', 'xx', None, None, 'x-part of x basis vector. VarIndexBase + 0.'),
+ ('Fixed', 'yx', None, None, 'y-part of x basis vector. VarIndexBase + 1.'),
+ ('Fixed', 'xy', None, None, 'x-part of y basis vector. VarIndexBase + 2.'),
+ ('Fixed', 'yy', None, None, 'y-part of y basis vector. VarIndexBase + 3.'),
+ ('Fixed', 'dx', None, None, 'Translation in x direction. VarIndexBase + 4.'),
+ ('Fixed', 'dy', None, None, 'Translation in y direction. VarIndexBase + 5.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1663,9 +1663,9 @@ otData = [
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
]),
('VarColorStop', [
- ('F2Dot14', 'StopOffset', None, None, 'VarIndexBase + 0'),
+ ('F2Dot14', 'StopOffset', None, None, 'VarIndexBase + 0.'),
('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
- ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 1'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 1.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1697,7 +1697,7 @@ otData = [
('PaintFormat3', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 3'),
('uint16', 'PaletteIndex', None, None, 'Index for a CPAL palette entry.'),
- ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 0'),
+ ('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved. VarIndexBase + 0.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1716,12 +1716,12 @@ otData = [
('PaintFormat5', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarLinearGradient table) to VarColorLine subtable.'),
- ('int16', 'x0', None, None, ''),
- ('int16', 'y0', None, None, ''),
- ('int16', 'x1', None, None, ''),
- ('int16', 'y1', None, None, ''),
- ('int16', 'x2', None, None, ''),
- ('int16', 'y2', None, None, ''),
+ ('int16', 'x0', None, None, 'VarIndexBase + 0.'),
+ ('int16', 'y0', None, None, 'VarIndexBase + 1.'),
+ ('int16', 'x1', None, None, 'VarIndexBase + 2.'),
+ ('int16', 'y1', None, None, 'VarIndexBase + 3.'),
+ ('int16', 'x2', None, None, 'VarIndexBase + 4.'),
+ ('int16', 'y2', None, None, 'VarIndexBase + 5.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1740,12 +1740,12 @@ otData = [
('PaintFormat7', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarRadialGradient table) to VarColorLine subtable.'),
- ('int16', 'x0', None, None, ''),
- ('int16', 'y0', None, None, ''),
- ('uint16', 'r0', None, None, ''),
- ('int16', 'x1', None, None, ''),
- ('int16', 'y1', None, None, ''),
- ('uint16', 'r1', None, None, ''),
+ ('int16', 'x0', None, None, 'VarIndexBase + 0.'),
+ ('int16', 'y0', None, None, 'VarIndexBase + 1.'),
+ ('uint16', 'r0', None, None, 'VarIndexBase + 2.'),
+ ('int16', 'x1', None, None, 'VarIndexBase + 3.'),
+ ('int16', 'y1', None, None, 'VarIndexBase + 4.'),
+ ('uint16', 'r1', None, None, 'VarIndexBase + 5.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1755,17 +1755,17 @@ otData = [
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintSweepGradient table) to ColorLine subtable.'),
('int16', 'centerX', None, None, 'Center x coordinate.'),
('int16', 'centerY', None, None, 'Center y coordinate.'),
- ('Angle', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
- ('Angle', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ('BiasedAngle', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
+ ('BiasedAngle', 'endAngle', None, None, 'End of the angular range of the gradient.'),
]),
# PaintVarSweepGradient
('PaintFormat9', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 9'),
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarSweepGradient table) to VarColorLine subtable.'),
- ('int16', 'centerX', None, None, 'Center x coordinate.'),
- ('int16', 'centerY', None, None, 'Center y coordinate.'),
- ('Angle', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
- ('Angle', 'endAngle', None, None, 'End of the angular range of the gradient.'),
+ ('int16', 'centerX', None, None, 'Center x coordinate. VarIndexBase + 0.'),
+ ('int16', 'centerY', None, None, 'Center y coordinate. VarIndexBase + 1.'),
+ ('BiasedAngle', 'startAngle', None, None, 'Start of the angular range of the gradient. VarIndexBase + 2.'),
+ ('BiasedAngle', 'endAngle', None, None, 'End of the angular range of the gradient. VarIndexBase + 3.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1806,8 +1806,8 @@ otData = [
('PaintFormat15', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 15'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTranslate table) to Paint subtable.'),
- ('int16', 'dx', None, None, 'Translation in x direction.'),
- ('int16', 'dy', None, None, 'Translation in y direction.'),
+ ('int16', 'dx', None, None, 'Translation in x direction. VarIndexBase + 0.'),
+ ('int16', 'dy', None, None, 'Translation in y direction. VarIndexBase + 1.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1822,8 +1822,8 @@ otData = [
('PaintFormat17', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 17'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScale table) to Paint subtable.'),
- ('F2Dot14', 'scaleX', None, None, ''),
- ('F2Dot14', 'scaleY', None, None, ''),
+ ('F2Dot14', 'scaleX', None, None, 'VarIndexBase + 0.'),
+ ('F2Dot14', 'scaleY', None, None, 'VarIndexBase + 1.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1840,10 +1840,10 @@ otData = [
('PaintFormat19', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 19'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleAroundCenter table) to Paint subtable.'),
- ('F2Dot14', 'scaleX', None, None, ''),
- ('F2Dot14', 'scaleY', None, None, ''),
- ('int16', 'centerX', None, None, ''),
- ('int16', 'centerY', None, None, ''),
+ ('F2Dot14', 'scaleX', None, None, 'VarIndexBase + 0.'),
+ ('F2Dot14', 'scaleY', None, None, 'VarIndexBase + 1.'),
+ ('int16', 'centerX', None, None, 'VarIndexBase + 2.'),
+ ('int16', 'centerY', None, None, 'VarIndexBase + 3.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1857,7 +1857,7 @@ otData = [
('PaintFormat21', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 21'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleUniform table) to Paint subtable.'),
- ('F2Dot14', 'scale', None, None, ''),
+ ('F2Dot14', 'scale', None, None, 'VarIndexBase + 0.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1873,9 +1873,9 @@ otData = [
('PaintFormat23', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 23'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarScaleUniformAroundCenter table) to Paint subtable.'),
- ('F2Dot14', 'scale', None, None, ''),
- ('int16', 'centerX', None, None, ''),
- ('int16', 'centerY', None, None, ''),
+ ('F2Dot14', 'scale', None, None, 'VarIndexBase + 0'),
+ ('int16', 'centerX', None, None, 'VarIndexBase + 1'),
+ ('int16', 'centerY', None, None, 'VarIndexBase + 2'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1889,7 +1889,7 @@ otData = [
('PaintFormat25', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 25'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotate table) to Paint subtable.'),
- ('Angle', 'angle', None, None, ''),
+ ('Angle', 'angle', None, None, 'VarIndexBase + 0.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1905,9 +1905,9 @@ otData = [
('PaintFormat27', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 27'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotateAroundCenter table) to Paint subtable.'),
- ('Angle', 'angle', None, None, ''),
- ('int16', 'centerX', None, None, ''),
- ('int16', 'centerY', None, None, ''),
+ ('Angle', 'angle', None, None, 'VarIndexBase + 0.'),
+ ('int16', 'centerX', None, None, 'VarIndexBase + 1.'),
+ ('int16', 'centerY', None, None, 'VarIndexBase + 2.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1922,8 +1922,8 @@ otData = [
('PaintFormat29', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 29'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkew table) to Paint subtable.'),
- ('Angle', 'xSkewAngle', None, None, ''),
- ('Angle', 'ySkewAngle', None, None, ''),
+ ('Angle', 'xSkewAngle', None, None, 'VarIndexBase + 0.'),
+ ('Angle', 'ySkewAngle', None, None, 'VarIndexBase + 1.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
@@ -1940,10 +1940,10 @@ otData = [
('PaintFormat31', [
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 31'),
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkewAroundCenter table) to Paint subtable.'),
- ('Angle', 'xSkewAngle', None, None, ''),
- ('Angle', 'ySkewAngle', None, None, ''),
- ('int16', 'centerX', None, None, ''),
- ('int16', 'centerY', None, None, ''),
+ ('Angle', 'xSkewAngle', None, None, 'VarIndexBase + 0.'),
+ ('Angle', 'ySkewAngle', None, None, 'VarIndexBase + 1.'),
+ ('int16', 'centerX', None, None, 'VarIndexBase + 2.'),
+ ('int16', 'centerY', None, None, 'VarIndexBase + 3.'),
('VarIndex', 'VarIndexBase', None, None, 'Base index into DeltaSetIndexMap.'),
]),
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
index fbd9db7b..6e7f3dfb 100644
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -600,6 +600,11 @@ class Coverage(FormatSwitchingBaseTable):
glyphs.append(attrs["value"])
+# The special 0xFFFFFFFF delta-set index is used to indicate that there
+# is no variation data in the ItemVariationStore for a given variable field
+NO_VARIATION_INDEX = 0xFFFFFFFF
+
+
class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
def populateDefaults(self, propagator=None):
@@ -647,12 +652,19 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
return rawTable
def toXML2(self, xmlWriter, font):
+ # Make xml dump less verbose, by omitting no-op entries like:
+ # <Map index="..." outer="65535" inner="65535"/>
+ xmlWriter.comment(
+ "Omitted values default to 0xFFFF/0xFFFF (no variations)"
+ )
+ xmlWriter.newline()
for i, value in enumerate(getattr(self, "mapping", [])):
- attrs = (
- ('index', i),
- ('outer', value >> 16),
- ('inner', value & 0xFFFF),
- )
+ attrs = [('index', i)]
+ if value != NO_VARIATION_INDEX:
+ attrs.extend([
+ ('outer', value >> 16),
+ ('inner', value & 0xFFFF),
+ ])
xmlWriter.simpletag("Map", attrs)
xmlWriter.newline()
@@ -661,8 +673,8 @@ class DeltaSetIndexMap(getFormatSwitchingBaseTableClass("uint8")):
if mapping is None:
self.mapping = mapping = []
index = safeEval(attrs['index'])
- outer = safeEval(attrs['outer'])
- inner = safeEval(attrs['inner'])
+ outer = safeEval(attrs.get('outer', '0xFFFF'))
+ inner = safeEval(attrs.get('inner', '0xFFFF'))
assert inner <= 0xFFFF
mapping.insert(index, (outer << 16) | inner)
@@ -1257,7 +1269,19 @@ class BaseGlyphList(BaseTable):
return self.__dict__.copy()
+class ClipBoxFormat(IntEnum):
+ Static = 1
+ Variable = 2
+
+ def is_variable(self):
+ return self is self.Variable
+
+ def as_variable(self):
+ return self.Variable
+
+
class ClipBox(getFormatSwitchingBaseTableClass("uint8")):
+ formatEnum = ClipBoxFormat
def as_tuple(self):
return tuple(getattr(self, conv.name) for conv in self.getConverters())
@@ -1492,12 +1516,24 @@ class PaintFormat(IntEnum):
PaintVarSkewAroundCenter = 31
PaintComposite = 32
+ def is_variable(self):
+ return self.name.startswith("PaintVar")
+
+ def as_variable(self):
+ if self.is_variable():
+ return self
+ try:
+ return PaintFormat.__members__[f"PaintVar{self.name[5:]}"]
+ except KeyError:
+ return None
+
class Paint(getFormatSwitchingBaseTableClass("uint8")):
+ formatEnum = PaintFormat
def getFormatName(self):
try:
- return PaintFormat(self.Format).name
+ return self.formatEnum(self.Format).name
except ValueError:
raise NotImplementedError(f"Unknown Paint format: {self.Format}")
@@ -1962,6 +1998,14 @@ def _buildClasses():
cls.DontShare = True
namespace[name] = cls
+ # link Var{Table} <-> {Table} (e.g. ColorStop <-> VarColorStop, etc.)
+ for name, _ in otData:
+ if name.startswith("Var") and len(name) > 3 and name[3:] in namespace:
+ varType = namespace[name]
+ noVarType = namespace[name[3:]]
+ varType.NoVarType = noVarType
+ noVarType.VarType = varType
+
for base, alts in _equivalents.items():
base = namespace[base]
for alt in alts:
diff --git a/Lib/fontTools/ttLib/tables/otTraverse.py b/Lib/fontTools/ttLib/tables/otTraverse.py
new file mode 100644
index 00000000..40b28b2b
--- /dev/null
+++ b/Lib/fontTools/ttLib/tables/otTraverse.py
@@ -0,0 +1,137 @@
+"""Methods for traversing trees of otData-driven OpenType tables."""
+from collections import deque
+from typing import Callable, Deque, Iterable, List, Optional, Tuple
+from .otBase import BaseTable
+
+
+__all__ = [
+ "bfs_base_table",
+ "dfs_base_table",
+ "SubTablePath",
+]
+
+
+class SubTablePath(Tuple[BaseTable.SubTableEntry, ...]):
+
+ def __str__(self) -> str:
+ path_parts = []
+ for entry in self:
+ path_part = entry.name
+ if entry.index is not None:
+ path_part += f"[{entry.index}]"
+ path_parts.append(path_part)
+ return ".".join(path_parts)
+
+
+# Given f(current frontier, new entries) add new entries to frontier
+AddToFrontierFn = Callable[[Deque[SubTablePath], List[SubTablePath]], None]
+
+
+def dfs_base_table(
+ root: BaseTable,
+ root_accessor: Optional[str] = None,
+ skip_root: bool = False,
+ predicate: Optional[Callable[[SubTablePath], bool]] = None,
+) -> Iterable[SubTablePath]:
+ """Depth-first search tree of BaseTables.
+
+ Args:
+ root (BaseTable): the root of the tree.
+ root_accessor (Optional[str]): attribute name for the root table, if any (mostly
+ useful for debugging).
+ skip_root (Optional[bool]): if True, the root itself is not visited, only its
+ children.
+ predicate (Optional[Callable[[SubTablePath], bool]]): function to filter out
+ paths. If True, the path is yielded and its subtables are added to the
+ queue. If False, the path is skipped and its subtables are not traversed.
+
+ Yields:
+ SubTablePath: tuples of BaseTable.SubTableEntry(name, table, index) namedtuples
+ for each of the nodes in the tree. The last entry in a path is the current
+ subtable, whereas preceding ones refer to its parent tables all the way up to
+ the root.
+ """
+ yield from _traverse_ot_data(
+ root,
+ root_accessor,
+ skip_root,
+ predicate,
+ lambda frontier, new: frontier.extendleft(reversed(new)),
+ )
+
+
+def bfs_base_table(
+ root: BaseTable,
+ root_accessor: Optional[str] = None,
+ skip_root: bool = False,
+ predicate: Optional[Callable[[SubTablePath], bool]] = None,
+) -> Iterable[SubTablePath]:
+ """Breadth-first search tree of BaseTables.
+
+ Args:
+ root (BaseTable): the root of the tree.
+ root_accessor (Optional[str]): attribute name for the root table, if any (mostly
+ useful for debugging).
+ skip_root (Optional[bool]): if True, the root itself is not visited, only its
+ children.
+ predicate (Optional[Callable[[SubTablePath], bool]]): function to filter out
+ paths. If True, the path is yielded and its subtables are added to the
+ queue. If False, the path is skipped and its subtables are not traversed.
+
+ Yields:
+ SubTablePath: tuples of BaseTable.SubTableEntry(name, table, index) namedtuples
+ for each of the nodes in the tree. The last entry in a path is the current
+ subtable, whereas preceding ones refer to its parent tables all the way up to
+ the root.
+ """
+ yield from _traverse_ot_data(
+ root,
+ root_accessor,
+ skip_root,
+ predicate,
+ lambda frontier, new: frontier.extend(new),
+ )
+
+
+def _traverse_ot_data(
+ root: BaseTable,
+ root_accessor: Optional[str],
+ skip_root: bool,
+ predicate: Optional[Callable[[SubTablePath], bool]],
+ add_to_frontier_fn: AddToFrontierFn,
+) -> Iterable[SubTablePath]:
+ # no visited because general otData cannot cycle (forward-offset only)
+ if root_accessor is None:
+ root_accessor = type(root).__name__
+
+ if predicate is None:
+
+ def predicate(path):
+ return True
+
+ frontier: Deque[SubTablePath] = deque()
+
+ root_entry = BaseTable.SubTableEntry(root_accessor, root)
+ if not skip_root:
+ frontier.append((root_entry,))
+ else:
+ add_to_frontier_fn(
+ frontier,
+ [(root_entry, subtable_entry) for subtable_entry in root.iterSubTables()],
+ )
+
+ while frontier:
+ # path is (value, attr_name) tuples. attr_name is attr of parent to get value
+ path = frontier.popleft()
+ current = path[-1].value
+
+ if not predicate(path):
+ continue
+
+ yield SubTablePath(path)
+
+ new_entries = [
+ path + (subtable_entry,) for subtable_entry in current.iterSubTables()
+ ]
+
+ add_to_frontier_fn(frontier, new_entries)
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py
index d7f7ef83..327d113f 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -4,6 +4,11 @@ from fontTools.misc.configTools import AbstractConfig
from fontTools.misc.textTools import Tag, byteord, tostr
from fontTools.misc.loggingTools import deprecateArgument
from fontTools.ttLib import TTLibError
+from fontTools.ttLib.ttGlyphSet import (
+ _TTGlyphSet, _TTGlyph,
+ _TTGlyphCFF, _TTGlyphGlyf,
+ _TTVarGlyphSet,
+)
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
from io import BytesIO, StringIO
import os
@@ -381,12 +386,14 @@ class TTFont(object):
keys = sortedTagList(keys)
return ["GlyphOrder"] + keys
- def ensureDecompiled(self):
+ def ensureDecompiled(self, recurse=None):
"""Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
for tag in self.keys():
table = self[tag]
- if self.lazy is not False and hasattr(table, "ensureDecompiled"):
- table.ensureDecompiled()
+ if recurse is None:
+ recurse = self.lazy is not False
+ if recurse and hasattr(table, "ensureDecompiled"):
+ table.ensureDecompiled(recurse=recurse)
self.lazy = False
def __len__(self):
@@ -673,7 +680,7 @@ class TTFont(object):
else:
raise KeyError(tag)
- def getGlyphSet(self, preferCFF=True):
+ def getGlyphSet(self, preferCFF=True, location=None, normalized=False):
"""Return a generic GlyphSet, which is a dict-like object
mapping glyph names to glyph objects. The returned glyph objects
have a .draw() method that supports the Pen protocol, and will
@@ -684,16 +691,28 @@ class TTFont(object):
If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use
the 'preferCFF' argument to specify which one should be taken. If the
font contains both a 'CFF ' and a 'CFF2' table, the latter is taken.
+
+ If the 'location' parameter is set, it should be a dictionary mapping
+ four-letter variation tags to their float values, and the returned
+ glyph-set will represent an instance of a variable font at that location.
+ If the 'normalized' variable is set to True, that location is interpretted
+ as in the normalized (-1..+1) space, otherwise it is in the font's defined
+ axes space.
"""
glyphs = None
if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or
("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))):
table_tag = "CFF2" if "CFF2" in self else "CFF "
+ if location:
+ raise NotImplementedError # TODO
glyphs = _TTGlyphSet(self,
list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF)
if glyphs is None and "glyf" in self:
- glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf)
+ if location and 'gvar' in self:
+ glyphs = _TTVarGlyphSet(self, location=location, normalized=normalized)
+ else:
+ glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf)
if glyphs is None:
raise TTLibError("Font contains no outlines")
@@ -726,109 +745,6 @@ class TTFont(object):
return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
-class _TTGlyphSet(object):
-
- """Generic dict-like GlyphSet class that pulls metrics from hmtx and
- glyph shape from TrueType or CFF.
- """
-
- def __init__(self, ttFont, glyphs, glyphType):
- """Construct a new glyphset.
-
- Args:
- font (TTFont): The font object (used to get metrics).
- glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
- glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
- """
- self._glyphs = glyphs
- self._hmtx = ttFont['hmtx']
- self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
- self._glyphType = glyphType
-
- def keys(self):
- return list(self._glyphs.keys())
-
- def has_key(self, glyphName):
- return glyphName in self._glyphs
-
- __contains__ = has_key
-
- def __getitem__(self, glyphName):
- horizontalMetrics = self._hmtx[glyphName]
- verticalMetrics = self._vmtx[glyphName] if self._vmtx else None
- return self._glyphType(
- self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics)
-
- def __len__(self):
- return len(self._glyphs)
-
- def get(self, glyphName, default=None):
- try:
- return self[glyphName]
- except KeyError:
- return default
-
-class _TTGlyph(object):
-
- """Wrapper for a TrueType glyph that supports the Pen protocol, meaning
- that it has .draw() and .drawPoints() methods that take a pen object as
- their only argument. Additionally there are 'width' and 'lsb' attributes,
- read from the 'hmtx' table.
-
- If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
- attributes.
- """
-
- def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
- """Construct a new _TTGlyph.
-
- Args:
- glyphset (_TTGlyphSet): A glyphset object used to resolve components.
- glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
- horizontalMetrics (int, int): The glyph's width and left sidebearing.
- """
- self._glyphset = glyphset
- self._glyph = glyph
- self.width, self.lsb = horizontalMetrics
- if verticalMetrics:
- self.height, self.tsb = verticalMetrics
- else:
- self.height, self.tsb = None, None
-
- def draw(self, pen):
- """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
- how that works.
- """
- self._glyph.draw(pen)
-
- def drawPoints(self, pen):
- # drawPoints is only implemented for _TTGlyphGlyf at this time.
- raise NotImplementedError()
-
-class _TTGlyphCFF(_TTGlyph):
- pass
-
-class _TTGlyphGlyf(_TTGlyph):
-
- def draw(self, pen):
- """Draw the glyph onto Pen. See fontTools.pens.basePen for details
- how that works.
- """
- glyfTable = self._glyphset._glyphs
- glyph = self._glyph
- offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
- glyph.draw(pen, glyfTable, offset)
-
- def drawPoints(self, pen):
- """Draw the glyph onto PointPen. See fontTools.pens.pointPen
- for details how that works.
- """
- glyfTable = self._glyphset._glyphs
- glyph = self._glyph
- offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
- glyph.drawPoints(pen, glyfTable, offset)
-
-
class GlyphOrder(object):
"""A pseudo table. The glyph order isn't in the font as a separate
diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py
new file mode 100644
index 00000000..be26215b
--- /dev/null
+++ b/Lib/fontTools/ttLib/ttGlyphSet.py
@@ -0,0 +1,221 @@
+"""GlyphSets returned by a TTFont."""
+
+from fontTools.misc.fixedTools import otRound
+from copy import copy
+
+class _TTGlyphSet(object):
+
+ """Generic dict-like GlyphSet class that pulls metrics from hmtx and
+ glyph shape from TrueType or CFF.
+ """
+
+ def __init__(self, ttFont, glyphs, glyphType):
+ """Construct a new glyphset.
+
+ Args:
+ font (TTFont): The font object (used to get metrics).
+ glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
+ glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
+ """
+ self._glyphs = glyphs
+ self._hmtx = ttFont['hmtx']
+ self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
+ self._glyphType = glyphType
+
+ def keys(self):
+ return list(self._glyphs.keys())
+
+ def has_key(self, glyphName):
+ return glyphName in self._glyphs
+
+ __contains__ = has_key
+
+ def __getitem__(self, glyphName):
+ horizontalMetrics = self._hmtx[glyphName]
+ verticalMetrics = self._vmtx[glyphName] if self._vmtx else None
+ return self._glyphType(
+ self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics)
+
+ def __len__(self):
+ return len(self._glyphs)
+
+ def get(self, glyphName, default=None):
+ try:
+ return self[glyphName]
+ except KeyError:
+ return default
+
+class _TTGlyph(object):
+
+ """Wrapper for a TrueType glyph that supports the Pen protocol, meaning
+ that it has .draw() and .drawPoints() methods that take a pen object as
+ their only argument. Additionally there are 'width' and 'lsb' attributes,
+ read from the 'hmtx' table.
+
+ If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
+ attributes.
+ """
+
+ def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
+ """Construct a new _TTGlyph.
+
+ Args:
+ glyphset (_TTGlyphSet): A glyphset object used to resolve components.
+ glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
+ horizontalMetrics (int, int): The glyph's width and left sidebearing.
+ """
+ self._glyphset = glyphset
+ self._glyph = glyph
+ self.width, self.lsb = horizontalMetrics
+ if verticalMetrics:
+ self.height, self.tsb = verticalMetrics
+ else:
+ self.height, self.tsb = None, None
+
+ def draw(self, pen):
+ """Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
+ how that works.
+ """
+ self._glyph.draw(pen)
+
+ def drawPoints(self, pen):
+ from fontTools.pens.pointPen import SegmentToPointPen
+ self.draw(SegmentToPointPen(pen))
+
+class _TTGlyphCFF(_TTGlyph):
+ pass
+
+class _TTGlyphGlyf(_TTGlyph):
+
+ def draw(self, pen):
+ """Draw the glyph onto Pen. See fontTools.pens.basePen for details
+ how that works.
+ """
+ glyfTable = self._glyphset._glyphs
+ glyph = self._glyph
+ offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
+ glyph.draw(pen, glyfTable, offset)
+
+ def drawPoints(self, pen):
+ """Draw the glyph onto PointPen. See fontTools.pens.pointPen
+ for details how that works.
+ """
+ glyfTable = self._glyphset._glyphs
+ glyph = self._glyph
+ offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
+ glyph.drawPoints(pen, glyfTable, offset)
+
+
+
+class _TTVarGlyphSet(_TTGlyphSet):
+
+ def __init__(self, font, location, normalized=False):
+ self._ttFont = font
+ self._glyphs = font['glyf']
+
+ if not normalized:
+ from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
+
+ axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes}
+ location = normalizeLocation(location, axes)
+ if 'avar' in font:
+ avar = font['avar']
+ avarSegments = avar.segments
+ new_location = {}
+ for axis_tag, value in location.items():
+ avarMapping = avarSegments.get(axis_tag, None)
+ if avarMapping is not None:
+ value = piecewiseLinearMap(value, avarMapping)
+ new_location[axis_tag] = value
+ location = new_location
+ del new_location
+
+ self.location = location
+
+ def __getitem__(self, glyphName):
+ if glyphName not in self._glyphs:
+ raise KeyError(glyphName)
+ return _TTVarGlyphGlyf(self._ttFont, glyphName, self.location)
+
+
+def _setCoordinates(glyph, coord, glyfTable):
+ # Handle phantom points for (left, right, top, bottom) positions.
+ assert len(coord) >= 4
+ if not hasattr(glyph, 'xMin'):
+ glyph.recalcBounds(glyfTable)
+ leftSideX = coord[-4][0]
+ rightSideX = coord[-3][0]
+ topSideY = coord[-2][1]
+ bottomSideY = coord[-1][1]
+
+ for _ in range(4):
+ del coord[-1]
+
+ if glyph.isComposite():
+ assert len(coord) == len(glyph.components)
+ for p,comp in zip(coord, glyph.components):
+ if hasattr(comp, 'x'):
+ comp.x,comp.y = p
+ elif glyph.numberOfContours == 0:
+ assert len(coord) == 0
+ else:
+ assert len(coord) == len(glyph.coordinates)
+ glyph.coordinates = coord
+
+ glyph.recalcBounds(glyfTable)
+
+ horizontalAdvanceWidth = otRound(rightSideX - leftSideX)
+ verticalAdvanceWidth = otRound(topSideY - bottomSideY)
+ leftSideBearing = otRound(glyph.xMin - leftSideX)
+ topSideBearing = otRound(topSideY - glyph.yMax)
+ return (
+ horizontalAdvanceWidth,
+ leftSideBearing,
+ verticalAdvanceWidth,
+ topSideBearing,
+ )
+
+
+class _TTVarGlyph(_TTGlyph):
+ def __init__(self, ttFont, glyphName, location):
+ self._ttFont = ttFont
+ self._glyphName = glyphName
+ self._location = location
+ # draw() fills these in
+ self.width = self.height = self.lsb = self.tsb = None
+
+
+class _TTVarGlyphGlyf(_TTVarGlyph):
+
+ def draw(self, pen):
+ from fontTools.varLib.iup import iup_delta
+ from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
+ from fontTools.varLib.models import supportScalar
+
+ glyf = self._ttFont['glyf']
+ hMetrics = self._ttFont['hmtx'].metrics
+ vMetrics = getattr(self._ttFont.get('vmtx'), 'metrics', None)
+
+ variations = self._ttFont['gvar'].variations[self._glyphName]
+ coordinates, _ = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics)
+ origCoords, endPts = None, None
+ for var in variations:
+ scalar = supportScalar(self._location, var.axes)
+ if not scalar:
+ continue
+ delta = var.coordinates
+ if None in delta:
+ if origCoords is None:
+ origCoords,control = glyf._getCoordinatesAndControls(self._glyphName, hMetrics, vMetrics)
+ endPts = control[1] if control[0] >= 1 else list(range(len(control[1])))
+ delta = iup_delta(delta, origCoords, endPts)
+ coordinates += GlyphCoordinates(delta) * scalar
+
+ glyph = copy(glyf[self._glyphName]) # Shallow copy
+ width, lsb, height, tsb = _setCoordinates(glyph, coordinates, glyf)
+ self.width = width
+ self.lsb = lsb
+ self.height = height
+ self.tsb = tsb
+ offset = lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
+ glyph.draw(pen, glyf, offset)
diff --git a/Lib/fontTools/ttLib/ttVisitor.py b/Lib/fontTools/ttLib/ttVisitor.py
new file mode 100644
index 00000000..54db61b1
--- /dev/null
+++ b/Lib/fontTools/ttLib/ttVisitor.py
@@ -0,0 +1,32 @@
+"""Specialization of fontTools.misc.visitor to work with TTFont."""
+
+from fontTools.misc.visitor import Visitor
+from fontTools.ttLib import TTFont
+
+
+class TTVisitor(Visitor):
+ def visitAttr(self, obj, attr, value, *args, **kwargs):
+ if isinstance(value, TTFont):
+ return False
+ super().visitAttr(obj, attr, value, *args, **kwargs)
+
+ def visit(self, obj, *args, **kwargs):
+ if hasattr(obj, "ensureDecompiled"):
+ obj.ensureDecompiled(recurse=False)
+ super().visit(obj, *args, **kwargs)
+
+
+@TTVisitor.register(TTFont)
+def visit(visitor, font, *args, **kwargs):
+ # Some objects have links back to TTFont; even though we
+ # have a check in visitAttr to stop them from recursing
+ # onto TTFont, sometimes they still do, for example when
+ # someone overrides visitAttr.
+ if hasattr(visitor, "font"):
+ return False
+
+ visitor.font = font
+ for tag in font.keys():
+ visitor.visit(font[tag], *args, **kwargs)
+ del visitor.font
+ return False
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
index bd04dd7a..fa6cb117 100755
--- a/Lib/fontTools/ufoLib/__init__.py
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -98,6 +98,11 @@ class UFOFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
FORMAT_2_0 = (2, 0)
FORMAT_3_0 = (3, 0)
+# python 3.11 doesn't like when a mixin overrides a dunder method like __str__
+# for some reasons it keep using Enum.__str__, see
+# https://github.com/fonttools/fonttools/pull/2655
+UFOFormatVersion.__str__ = _VersionTupleEnumMixin.__str__
+
class UFOFileStructure(enum.Enum):
ZIP = "zip"
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
index 89c9176a..7d28eaf7 100755
--- a/Lib/fontTools/ufoLib/glifLib.py
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -79,6 +79,9 @@ class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
versions.add(cls.FORMAT_2_0)
return frozenset(versions)
+# workaround for py3.11, see https://github.com/fonttools/fonttools/pull/2655
+GLIFFormatVersion.__str__ = _VersionTupleEnumMixin.__str__
+
# ------------
# Simple Glyph
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 4029a107..f1ca99ff 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -30,13 +30,15 @@ from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import OTTableWriter
from fontTools.varLib import builder, models, varStore
-from fontTools.varLib.merger import VariationMerger
+from fontTools.varLib.merger import VariationMerger, COLRVariationMerger
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.iup import iup_delta_optimize
from fontTools.varLib.featureVars import addFeatureVariations
from fontTools.designspaceLib import DesignSpaceDocument, InstanceDescriptor
from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts
from fontTools.varLib.stat import buildVFStatTable
+from fontTools.colorLib.builder import buildColrV1
+from fontTools.colorLib.unbuilder import unbuildColrV1
from functools import partial
from collections import OrderedDict, namedtuple
import os.path
@@ -486,7 +488,7 @@ def _get_advance_metrics(font, masterModel, master_ttfs,
vOrigMap[glyphName] = storeBuilder.storeDeltas(deltas, round=noRound)
indirectStore = storeBuilder.finish()
- mapping2 = indirectStore.optimize()
+ mapping2 = indirectStore.optimize(use_NO_VARIATION_INDEX=False)
advMapping = [mapping2[advMapping[g]] for g in glyphOrder]
advanceMapping = builder.buildVarIdxMap(advMapping, glyphOrder)
@@ -606,7 +608,7 @@ def _add_BASE(font, masterModel, master_ttfs, axisTags):
merger.mergeTables(font, master_ttfs, ['BASE'])
store = merger.store_builder.finish()
- if not store.VarData:
+ if not store:
return
base = font['BASE'].table
assert base.Version == 0x00010000
@@ -621,7 +623,7 @@ def _merge_OTL(font, model, master_fonts, axisTags):
merger.mergeTables(font, master_fonts, ['GSUB', 'GDEF', 'GPOS'])
store = merger.store_builder.finish()
- if not store.VarData:
+ if not store:
return
try:
GDEF = font['GDEF'].table
@@ -711,6 +713,19 @@ def _add_CFF2(varFont, model, master_fonts):
merge_region_fonts(varFont, model, ordered_fonts_list, glyphOrder)
+def _add_COLR(font, model, master_fonts, axisTags, colr_layer_reuse=True):
+ merger = COLRVariationMerger(model, axisTags, font, allowLayerReuse=colr_layer_reuse)
+ merger.mergeTables(font, master_fonts)
+ store = merger.store_builder.finish()
+
+ colr = font["COLR"].table
+ if store:
+ mapping = store.optimize()
+ colr.VarStore = store
+ varIdxes = [mapping[v] for v in merger.varIdxes]
+ colr.VarIndexMap = builder.buildDeltaSetIndexMap(varIdxes)
+
+
def load_designspace(designspace):
# TODO: remove this and always assume 'designspace' is a DesignSpaceDocument,
# never a file path, as that's already handled by caller
@@ -865,7 +880,14 @@ def set_default_weight_width_slant(font, location):
font["post"].italicAngle = italicAngle
-def build_many(designspace: DesignSpaceDocument, master_finder=lambda s:s, exclude=[], optimize=True, skip_vf=lambda vf_name: False):
+def build_many(
+ designspace: DesignSpaceDocument,
+ master_finder=lambda s:s,
+ exclude=[],
+ optimize=True,
+ skip_vf=lambda vf_name: False,
+ colr_layer_reuse=True,
+):
"""
Build variable fonts from a designspace file, version 5 which can define
several VFs, or version 4 which has implicitly one VF covering the whole doc.
@@ -890,14 +912,21 @@ def build_many(designspace: DesignSpaceDocument, master_finder=lambda s:s, exclu
vfDoc,
master_finder,
exclude=list(exclude) + ["STAT"],
- optimize=optimize
+ optimize=optimize,
+ colr_layer_reuse=colr_layer_reuse,
)[0]
if "STAT" not in exclude:
buildVFStatTable(vf, designspace, name)
res[name] = vf
return res
-def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
+def build(
+ designspace,
+ master_finder=lambda s:s,
+ exclude=[],
+ optimize=True,
+ colr_layer_reuse=True,
+):
"""
Build variation font from a designspace file.
@@ -975,6 +1004,8 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
post.formatType = 2.0
post.extraNames = []
post.mapping = {}
+ if 'COLR' not in exclude and 'COLR' in vf and vf['COLR'].version > 0:
+ _add_COLR(vf, model, master_fonts, axisTags, colr_layer_reuse)
set_default_weight_width_slant(
vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes}
@@ -1083,6 +1114,12 @@ def main(args=None):
help='do not perform IUP optimization'
)
parser.add_argument(
+ '--no-colr-layer-reuse',
+ dest='colr_layer_reuse',
+ action='store_false',
+ help='do not rebuild variable COLR table to optimize COLR layer reuse',
+ )
+ parser.add_argument(
'--master-finder',
default='master_ttf_interpolatable/{stem}.ttf',
help=(
@@ -1120,7 +1157,8 @@ def main(args=None):
designspace_filename,
finder,
exclude=options.exclude,
- optimize=options.optimize
+ optimize=options.optimize,
+ colr_layer_reuse=options.colr_layer_reuse,
)
outfile = options.outfile
diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py
index 08ddfc41..727efa70 100644
--- a/Lib/fontTools/varLib/cff.py
+++ b/Lib/fontTools/varLib/cff.py
@@ -639,6 +639,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
# convert to deltas
deltas = get_delta_func(coord)[1:]
coord = [coord[0]] + deltas
+ coord.append(1)
new_coords.append(coord)
cmd[1] = new_coords
lastOp = op
diff --git a/Lib/fontTools/varLib/errors.py b/Lib/fontTools/varLib/errors.py
index c5a149cb..4f30f901 100644
--- a/Lib/fontTools/varLib/errors.py
+++ b/Lib/fontTools/varLib/errors.py
@@ -30,12 +30,8 @@ class VarLibMergeError(VarLibError):
def _master_name(self, ix):
if self.merger is not None:
ttf = self.merger.ttfs[ix]
- if (
- "name" in ttf
- and ttf["name"].getDebugName(1)
- and ttf["name"].getDebugName(2)
- ):
- return ttf["name"].getDebugName(1) + " " + ttf["name"].getDebugName(2)
+ if "name" in ttf and ttf["name"].getBestFullName():
+ return ttf["name"].getBestFullName()
elif hasattr(ttf.reader, "file") and hasattr(ttf.reader.file, "name"):
return ttf.reader.file.name
return f"master number {ix}"
@@ -46,7 +42,10 @@ class VarLibMergeError(VarLibError):
index = [x == self.cause["expected"] for x in self.cause["got"]].index(
False
)
- return index, self._master_name(index)
+ master_name = self._master_name(index)
+ if "location" in self.cause:
+ master_name = f"{master_name} ({self.cause['location']})"
+ return index, master_name
return None, None
@property
@@ -54,7 +53,7 @@ class VarLibMergeError(VarLibError):
if "expected" in self.cause and "got" in self.cause:
offender_index, offender = self.offender
got = self.cause["got"][offender_index]
- return f"Expected to see {self.stack[0]}=={self.cause['expected']}, instead saw {got}\n"
+ return f"Expected to see {self.stack[0]}=={self.cause['expected']!r}, instead saw {got!r}\n"
return ""
def __str__(self):
@@ -76,11 +75,21 @@ class ShouldBeConstant(VarLibMergeError):
@property
def details(self):
+ basic_message = super().details
+
if self.stack[0] != ".FeatureCount" or self.merger is None:
- return super().details
- offender_index, offender = self.offender
+ return basic_message
+
+ assert self.stack[0] == ".FeatureCount"
+ offender_index, _ = self.offender
bad_ttf = self.merger.ttfs[offender_index]
- good_ttf = self.merger.ttfs[offender_index - 1]
+ good_ttf = next(
+ ttf
+ for ttf in self.merger.ttfs
+ if self.stack[-1] in ttf
+ and ttf[self.stack[-1]].table.FeatureList.FeatureCount
+ == self.cause["expected"]
+ )
good_features = [
x.FeatureTag
@@ -90,7 +99,7 @@ class ShouldBeConstant(VarLibMergeError):
x.FeatureTag
for x in bad_ttf[self.stack[-1]].table.FeatureList.FeatureRecord
]
- return (
+ return basic_message + (
"\nIncompatible features between masters.\n"
f"Expected: {', '.join(good_features)}.\n"
f"Got: {', '.join(bad_features)}.\n"
@@ -111,6 +120,20 @@ class FoundANone(VarLibMergeError):
return f"{stack[0]}=={cause['got']}\n"
+class NotANone(VarLibMergeError):
+ """one of the values in a list was not empty when it should have been"""
+
+ @property
+ def offender(self):
+ index = [x is not None for x in self.cause["got"]].index(True)
+ return index, self._master_name(index)
+
+ @property
+ def details(self):
+ cause, stack = self.cause, self.stack
+ return f"{stack[0]}=={cause['got']}\n"
+
+
class MismatchedTypes(VarLibMergeError):
"""data had inconsistent types"""
@@ -134,12 +157,20 @@ class InconsistentExtensions(VarLibMergeError):
class UnsupportedFormat(VarLibMergeError):
"""an OpenType subtable (%s) had a format I didn't expect"""
+ def __init__(self, merger=None, **kwargs):
+ super().__init__(merger, **kwargs)
+ if not self.stack:
+ self.stack = [".Format"]
+
@property
def reason(self):
- return self.__doc__ % self.cause["subtable"]
+ s = self.__doc__ % self.cause["subtable"]
+ if "value" in self.cause:
+ s += f" ({self.cause['value']!r})"
+ return s
-class UnsupportedFormat(UnsupportedFormat):
+class InconsistentFormats(UnsupportedFormat):
"""an OpenType subtable (%s) had inconsistent formats between masters"""
diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py
index e3366327..ad47ab8e 100644
--- a/Lib/fontTools/varLib/featureVars.py
+++ b/Lib/fontTools/varLib/featureVars.py
@@ -44,6 +44,10 @@ def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
# >>> f.save(dstPath)
"""
+ _checkSubstitutionGlyphsExist(
+ glyphNames=set(font.getGlyphOrder()),
+ substitutions=conditionalSubstitutions,
+ )
substitutions = overlayFeatureVariations(conditionalSubstitutions)
@@ -66,6 +70,18 @@ def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
conditionsAndLookups,
featureTag)
+def _checkSubstitutionGlyphsExist(glyphNames, substitutions):
+ referencedGlyphNames = set()
+ for _, substitution in substitutions:
+ referencedGlyphNames |= substitution.keys()
+ referencedGlyphNames |= set(substitution.values())
+ missing = referencedGlyphNames - glyphNames
+ if missing:
+ raise VarLibValidationError(
+ "Missing glyphs are referenced in conditional substitution rules:"
+ f" {', '.join(missing)}"
+ )
+
def overlayFeatureVariations(conditionalSubstitutions):
"""Compute overlaps between all conditional substitutions.
diff --git a/Lib/fontTools/varLib/instancer/__init__.py b/Lib/fontTools/varLib/instancer/__init__.py
index 6dad393e..8f976123 100644
--- a/Lib/fontTools/varLib/instancer/__init__.py
+++ b/Lib/fontTools/varLib/instancer/__init__.py
@@ -90,12 +90,11 @@ from fontTools.varLib import builder
from fontTools.varLib.mvar import MVAR_ENTRIES
from fontTools.varLib.merger import MutatorMerger
from fontTools.varLib.instancer import names
-from contextlib import contextmanager
+from fontTools.misc.cliTools import makeOutputFileName
import collections
from copy import deepcopy
from enum import IntEnum
import logging
-from itertools import islice
import os
import re
@@ -329,7 +328,9 @@ def limitTupleVariationAxisRange(var, axisTag, axisRange):
return [var, newVar]
-def _instantiateGvarGlyph(glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=True):
+def _instantiateGvarGlyph(
+ glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=True
+):
coordinates, ctrl = glyf._getCoordinatesAndControls(glyphname, hMetrics, vMetrics)
endPts = ctrl.endPts
@@ -365,22 +366,26 @@ def _instantiateGvarGlyph(glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits,
for var in tupleVarStore:
var.optimize(coordinates, endPts, isComposite)
+
def instantiateGvarGlyph(varfont, glyphname, axisLimits, optimize=True):
"""Remove?
https://github.com/fonttools/fonttools/pull/2266"""
gvar = varfont["gvar"]
glyf = varfont["glyf"]
- hMetrics = varfont['hmtx'].metrics
- vMetrics = getattr(varfont.get('vmtx'), 'metrics', None)
- _instantiateGvarGlyph(glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=optimize)
+ hMetrics = varfont["hmtx"].metrics
+ vMetrics = getattr(varfont.get("vmtx"), "metrics", None)
+ _instantiateGvarGlyph(
+ glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=optimize
+ )
+
def instantiateGvar(varfont, axisLimits, optimize=True):
log.info("Instantiating glyf/gvar tables")
gvar = varfont["gvar"]
glyf = varfont["glyf"]
- hMetrics = varfont['hmtx'].metrics
- vMetrics = getattr(varfont.get('vmtx'), 'metrics', None)
+ hMetrics = varfont["hmtx"].metrics
+ vMetrics = getattr(varfont.get("vmtx"), "metrics", None)
# Get list of glyph names sorted by component depth.
# If a composite glyph is processed before its base glyph, the bounds may
# be calculated incorrectly because deltas haven't been applied to the
@@ -395,7 +400,9 @@ def instantiateGvar(varfont, axisLimits, optimize=True):
),
)
for glyphname in glyphnames:
- _instantiateGvarGlyph(glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=optimize)
+ _instantiateGvarGlyph(
+ glyphname, glyf, gvar, hMetrics, vMetrics, axisLimits, optimize=optimize
+ )
if not gvar.variations:
del varfont["gvar"]
@@ -485,7 +492,7 @@ def _instantiateVHVAR(varfont, axisLimits, tableFields):
# or AdvHeightMap. If a direct, implicit glyphID->VariationIndex mapping is
# used for advances, skip re-optimizing and maintain original VariationIndex.
if getattr(vhvar, tableFields.advMapping):
- varIndexMapping = varStore.optimize()
+ varIndexMapping = varStore.optimize(use_NO_VARIATION_INDEX=False)
glyphOrder = varfont.getGlyphOrder()
_remapVarIdxMap(vhvar, tableFields.advMapping, varIndexMapping, glyphOrder)
if getattr(vhvar, tableFields.sb1): # left or top sidebearings
@@ -633,6 +640,7 @@ def instantiateItemVariationStore(itemVarStore, fvarAxes, axisLimits):
for major, deltas in enumerate(defaultDeltaArray)
for minor, delta in enumerate(deltas)
}
+ defaultDeltas[itemVarStore.NO_VARIATION_INDEX] = 0
return defaultDeltas
@@ -745,23 +753,7 @@ def _limitFeatureVariationConditionRange(condition, axisRange):
values = [minValue, maxValue]
for i, value in enumerate(values):
- if value < 0:
- if axisRange.minimum == 0:
- newValue = 0
- else:
- newValue = value / abs(axisRange.minimum)
- if newValue <= -1.0:
- newValue = -1.0
- elif value > 0:
- if axisRange.maximum == 0:
- newValue = 0
- else:
- newValue = value / axisRange.maximum
- if newValue >= 1.0:
- newValue = 1.0
- else:
- newValue = 0
- values[i] = newValue
+ values[i] = normalizeValue(value, (axisRange.minimum, 0, axisRange.maximum))
return AxisRange(*values)
@@ -806,12 +798,12 @@ def _instantiateFeatureVariationRecord(
return applies, shouldKeep
-def _limitFeatureVariationRecord(record, axisRanges, fvarAxes):
+def _limitFeatureVariationRecord(record, axisRanges, axisOrder):
newConditions = []
for i, condition in enumerate(record.ConditionSet.ConditionTable):
if condition.Format == 1:
axisIdx = condition.AxisIndex
- axisTag = fvarAxes[axisIdx].axisTag
+ axisTag = axisOrder[axisIdx]
if axisTag in axisRanges:
axisRange = axisRanges[axisTag]
newRange = _limitFeatureVariationConditionRange(condition, axisRange)
@@ -855,7 +847,7 @@ def _instantiateFeatureVariations(table, fvarAxes, axisLimits):
record, i, location, fvarAxes, axisIndexMap
)
if shouldKeep:
- shouldKeep = _limitFeatureVariationRecord(record, axisRanges, fvarAxes)
+ shouldKeep = _limitFeatureVariationRecord(record, axisRanges, axisOrder)
if shouldKeep and _featureVariationRecordIsUnique(record, uniqueRecords):
newRecords.append(record)
@@ -938,24 +930,16 @@ def instantiateAvar(varfont, axisLimits):
)
newMapping = {}
for fromCoord, toCoord in mapping.items():
- if fromCoord < 0:
- if axisRange.minimum == 0 or fromCoord < axisRange.minimum:
- continue
- else:
- fromCoord /= abs(axisRange.minimum)
- elif fromCoord > 0:
- if axisRange.maximum == 0 or fromCoord > axisRange.maximum:
- continue
- else:
- fromCoord /= axisRange.maximum
- if toCoord < 0:
- assert mappedMin != 0
- assert toCoord >= mappedMin
- toCoord /= abs(mappedMin)
- elif toCoord > 0:
- assert mappedMax != 0
- assert toCoord <= mappedMax
- toCoord /= mappedMax
+
+ if fromCoord < axisRange.minimum or fromCoord > axisRange.maximum:
+ continue
+ fromCoord = normalizeValue(
+ fromCoord, (axisRange.minimum, 0, axisRange.maximum)
+ )
+
+ assert mappedMin <= toCoord <= mappedMax
+ toCoord = normalizeValue(toCoord, (mappedMin, 0, mappedMax))
+
fromCoord = floatToFixedToFloat(fromCoord, 14)
toCoord = floatToFixedToFloat(toCoord, 14)
newMapping[fromCoord] = toCoord
@@ -1199,10 +1183,10 @@ def instantiateVariableFont(
requires the skia-pathops package (available to pip install).
The overlap parameter only has effect when generating full static instances.
updateFontNames (bool): if True, update the instantiated font's name table using
- the Axis Value Tables from the STAT table. The name table will be updated so
- it conforms to the R/I/B/BI model. If the STAT table is missing or
- an Axis Value table is missing for a given axis coordinate, a ValueError will
- be raised.
+ the Axis Value Tables from the STAT table. The name table and the style bits
+ in the head and OS/2 table will be updated so they conform to the R/I/B/BI
+ model. If the STAT table is missing or an Axis Value table is missing for
+ a given axis coordinate, a ValueError will be raised.
"""
# 'overlap' used to be bool and is now enum; for backward compat keep accepting bool
overlap = OverlapMode(int(overlap))
@@ -1272,9 +1256,51 @@ def instantiateVariableFont(
},
)
+ if updateFontNames:
+ # Set Regular/Italic/Bold/Bold Italic bits as appropriate, after the
+ # name table has been updated.
+ setRibbiBits(varfont)
+
return varfont
+def setRibbiBits(font):
+ """Set the `head.macStyle` and `OS/2.fsSelection` style bits
+ appropriately."""
+
+ english_ribbi_style = font["name"].getName(names.NameID.SUBFAMILY_NAME, 3, 1, 0x409)
+ if english_ribbi_style is None:
+ return
+
+ styleMapStyleName = english_ribbi_style.toStr().lower()
+ if styleMapStyleName not in {"regular", "bold", "italic", "bold italic"}:
+ return
+
+ if styleMapStyleName == "bold":
+ font["head"].macStyle = 0b01
+ elif styleMapStyleName == "bold italic":
+ font["head"].macStyle = 0b11
+ elif styleMapStyleName == "italic":
+ font["head"].macStyle = 0b10
+
+ selection = font["OS/2"].fsSelection
+ # First clear...
+ selection &= ~(1 << 0)
+ selection &= ~(1 << 5)
+ selection &= ~(1 << 6)
+ # ...then re-set the bits.
+ if styleMapStyleName == "regular":
+ selection |= 1 << 6
+ elif styleMapStyleName == "bold":
+ selection |= 1 << 5
+ elif styleMapStyleName == "italic":
+ selection |= 1 << 0
+ elif styleMapStyleName == "bold italic":
+ selection |= 1 << 0
+ selection |= 1 << 5
+ font["OS/2"].fsSelection = selection
+
+
def splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange):
location, axisRanges = {}, {}
for axisTag, value in axisLimits.items():
@@ -1380,6 +1406,18 @@ def parseArgs(args):
help="Update the instantiated font's `name` table. Input font must have "
"a STAT table with Axis Value Tables",
)
+ parser.add_argument(
+ "--no-recalc-timestamp",
+ dest="recalc_timestamp",
+ action="store_false",
+ help="Don't set the output font's timestamp to the current time.",
+ )
+ parser.add_argument(
+ "--no-recalc-bounds",
+ dest="recalc_bounds",
+ action="store_false",
+ help="Don't recalculate font bounding boxes",
+ )
loggingGroup = parser.add_mutually_exclusive_group(required=False)
loggingGroup.add_argument(
"-v", "--verbose", action="store_true", help="Run more verbosely."
@@ -1417,12 +1455,16 @@ def parseArgs(args):
def main(args=None):
- """Partially instantiate a variable font."""
+ """Partially instantiate a variable font"""
infile, axisLimits, options = parseArgs(args)
log.info("Restricting axes: %s", axisLimits)
log.info("Loading variable font")
- varfont = TTFont(infile)
+ varfont = TTFont(
+ infile,
+ recalcTimestamp=options.recalc_timestamp,
+ recalcBBoxes=options.recalc_bounds,
+ )
isFullInstance = {
axisTag for axisTag, limit in axisLimits.items() if not isinstance(limit, tuple)
@@ -1437,9 +1479,9 @@ def main(args=None):
updateFontNames=options.update_name_table,
)
+ suffix = "-instance" if isFullInstance else "-partial"
outfile = (
- os.path.splitext(infile)[0]
- + "-{}.ttf".format("instance" if isFullInstance else "partial")
+ makeOutputFileName(infile, overWrite=True, suffix=suffix)
if not options.output
else options.output
)
diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py
index a9583a18..f86b6f9b 100644
--- a/Lib/fontTools/varLib/interpolatable.py
+++ b/Lib/fontTools/varLib/interpolatable.py
@@ -16,12 +16,12 @@ import itertools
import sys
def _rot_list(l, k):
- """Rotate list by k items forward. Ie. item at position 0 will be
- at position k in returned list. Negative k is allowed."""
- n = len(l)
- k %= n
- if not k: return l
- return l[n-k:] + l[:n-k]
+ """Rotate list by k items forward. Ie. item at position 0 will be
+ at position k in returned list. Negative k is allowed."""
+ n = len(l)
+ k %= n
+ if not k: return l
+ return l[n-k:] + l[:n-k]
class PerContourPen(BasePen):
@@ -361,20 +361,69 @@ def main(args=None):
from os.path import basename
- names = [basename(filename).rsplit(".", 1)[0] for filename in args.inputs]
-
fonts = []
+ names = []
+
+ if len(args.inputs) == 1:
+ if args.inputs[0].endswith('.designspace'):
+ from fontTools.designspaceLib import DesignSpaceDocument
+ designspace = DesignSpaceDocument.fromfile(args.inputs[0])
+ args.inputs = [master.path for master in designspace.sources]
+
+ elif args.inputs[0].endswith('.glyphs'):
+ from glyphsLib import GSFont, to_ufos
+ gsfont = GSFont(args.inputs[0])
+ fonts.extend(to_ufos(gsfont))
+ names = ['%s-%s' % (f.info.familyName, f.info.styleName) for f in fonts]
+ args.inputs = []
+
+ elif args.inputs[0].endswith('.ttf'):
+ from fontTools.ttLib import TTFont
+ font = TTFont(args.inputs[0])
+ if 'gvar' in font:
+ # Is variable font
+ gvar = font['gvar']
+ # Gather all "master" locations
+ locs = set()
+ for variations in gvar.variations.values():
+ for var in variations:
+ loc = []
+ for tag,val in sorted(var.axes.items()):
+ loc.append((tag,val[1]))
+ locs.add(tuple(loc))
+ # Rebuild locs as dictionaries
+ new_locs = [{}]
+ for loc in sorted(locs, key=lambda v: (len(v), v)):
+ names.append(str(loc))
+ l = {}
+ for tag,val in loc:
+ l[tag] = val
+ new_locs.append(l)
+ locs = new_locs
+ del new_locs
+ # locs is all master locations now
+
+ for loc in locs:
+ fonts.append(font.getGlyphSet(location=loc, normalized=True))
+
+ args.inputs = []
+
+
for filename in args.inputs:
if filename.endswith(".ufo"):
from fontTools.ufoLib import UFOReader
-
fonts.append(UFOReader(filename))
else:
from fontTools.ttLib import TTFont
-
fonts.append(TTFont(filename))
- glyphsets = [font.getGlyphSet() for font in fonts]
+ names.append(basename(filename).rsplit(".", 1)[0])
+
+ if hasattr(fonts[0], 'getGlyphSet'):
+ glyphsets = [font.getGlyphSet() for font in fonts]
+ else:
+ glyphsets = fonts
+
problems = test(glyphsets, glyphs=glyphs, names=names)
if args.json:
import json
diff --git a/Lib/fontTools/varLib/iup.py b/Lib/fontTools/varLib/iup.py
index 45a7a5ed..9c5bc35b 100644
--- a/Lib/fontTools/varLib/iup.py
+++ b/Lib/fontTools/varLib/iup.py
@@ -1,4 +1,47 @@
-def iup_segment(coords, rc1, rd1, rc2, rd2):
+from typing import (
+ Sequence,
+ Tuple,
+ Union,
+)
+from numbers import (
+ Integral,
+ Real
+)
+
+try:
+ import cython
+except ImportError:
+ # if cython not installed, use mock module with no-op decorators and types
+ from fontTools.misc import cython
+
+if cython.compiled:
+ # Yep, I'm compiled.
+ COMPILED = True
+else:
+ # Just a lowly interpreted script.
+ COMPILED = False
+
+
+_Point = Tuple[Real, Real]
+_Delta = Tuple[Real, Real]
+_PointSegment = Sequence[_Point]
+_DeltaSegment = Sequence[_Delta]
+_DeltaOrNone = Union[_Delta, None]
+_DeltaOrNoneSegment = Sequence[_DeltaOrNone]
+_Endpoints = Sequence[Integral]
+
+
+MAX_LOOKBACK = 8
+
+def iup_segment(coords : _PointSegment,
+ rc1 : _Point,
+ rd1 : _Delta,
+ rc2 : _Point,
+ rd2 : _Delta) -> _DeltaSegment:
+ """Given two reference coordinates `rc1` & `rc2` and their respective
+ delta vectors `rd1` & `rd2`, returns interpolated deltas for the set of
+ coordinates `coords`. """
+
# rc1 = reference coord 1
# rd1 = reference delta 1
out_arrays = [None, None]
@@ -6,7 +49,6 @@ def iup_segment(coords, rc1, rd1, rc2, rd2):
out_arrays[j] = out = []
x1, x2, d1, d2 = rc1[j], rc2[j], rd1[j], rd2[j]
-
if x1 == x2:
n = len(coords)
if d1 == d2:
@@ -36,14 +78,20 @@ def iup_segment(coords, rc1, rd1, rc2, rd2):
return zip(*out_arrays)
-def iup_contour(delta, coords):
- assert len(delta) == len(coords)
- if None not in delta:
- return delta
+def iup_contour(deltas : _DeltaOrNoneSegment,
+ coords : _PointSegment) -> _DeltaSegment:
+ """For the contour given in `coords`, interpolate any missing
+ delta values in delta vector `deltas`.
+
+ Returns fully filled-out delta vector."""
- n = len(delta)
+ assert len(deltas) == len(coords)
+ if None not in deltas:
+ return deltas
+
+ n = len(deltas)
# indices of points with explicit deltas
- indices = [i for i,v in enumerate(delta) if v is not None]
+ indices = [i for i,v in enumerate(deltas) if v is not None]
if not indices:
# All deltas are None. Return 0,0 for all.
return [(0,0)]*n
@@ -54,23 +102,31 @@ def iup_contour(delta, coords):
if start != 0:
# Initial segment that wraps around
i1, i2, ri1, ri2 = 0, start, start, indices[-1]
- out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
- out.append(delta[start])
+ out.extend(iup_segment(coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2]))
+ out.append(deltas[start])
for end in it:
if end - start > 1:
i1, i2, ri1, ri2 = start+1, end, start, end
- out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
- out.append(delta[end])
+ out.extend(iup_segment(coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2]))
+ out.append(deltas[end])
start = end
if start != n-1:
# Final segment that wraps around
i1, i2, ri1, ri2 = start+1, n, start, indices[0]
- out.extend(iup_segment(coords[i1:i2], coords[ri1], delta[ri1], coords[ri2], delta[ri2]))
+ out.extend(iup_segment(coords[i1:i2], coords[ri1], deltas[ri1], coords[ri2], deltas[ri2]))
- assert len(delta) == len(out), (len(delta), len(out))
+ assert len(deltas) == len(out), (len(deltas), len(out))
return out
-def iup_delta(delta, coords, ends):
+def iup_delta(deltas : _DeltaOrNoneSegment,
+ coords : _PointSegment,
+ ends: _Endpoints) -> _DeltaSegment:
+ """For the outline given in `coords`, with contour endpoints given
+ in sorted increasing order in `ends`, interpolate any missing
+ delta values in delta vector `deltas`.
+
+ Returns fully filled-out delta vector."""
+
assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4
n = len(coords)
ends = ends + [n-4, n-3, n-2, n-1]
@@ -78,7 +134,7 @@ def iup_delta(delta, coords, ends):
start = 0
for end in ends:
end += 1
- contour = iup_contour(delta[start:end], coords[start:end])
+ contour = iup_contour(deltas[start:end], coords[start:end])
out.extend(contour)
start = end
@@ -86,7 +142,15 @@ def iup_delta(delta, coords, ends):
# Optimizer
-def can_iup_in_between(deltas, coords, i, j, tolerance):
+def can_iup_in_between(deltas : _DeltaSegment,
+ coords : _PointSegment,
+ i : Integral,
+ j : Integral,
+ tolerance : Real) -> bool:
+ """Return true if the deltas for points at `i` and `j` (`i < j`) can be
+ successfully used to interpolate deltas for points in between them within
+ provided error tolerance."""
+
assert j - i >= 2
interp = list(iup_segment(coords[i+1:j], coords[i], deltas[i], coords[j], deltas[j]))
deltas = deltas[i+1:j]
@@ -95,23 +159,25 @@ def can_iup_in_between(deltas, coords, i, j, tolerance):
return all(abs(complex(x-p, y-q)) <= tolerance for (x,y),(p,q) in zip(deltas, interp))
-def _iup_contour_bound_forced_set(delta, coords, tolerance=0):
+def _iup_contour_bound_forced_set(deltas : _DeltaSegment,
+ coords : _PointSegment,
+ tolerance : Real = 0) -> set:
"""The forced set is a conservative set of points on the contour that must be encoded
explicitly (ie. cannot be interpolated). Calculating this set allows for significantly
speeding up the dynamic-programming, as well as resolve circularity in DP.
The set is precise; that is, if an index is in the returned set, then there is no way
- that IUP can generate delta for that point, given coords and delta.
+ that IUP can generate delta for that point, given `coords` and `deltas`.
"""
- assert len(delta) == len(coords)
+ assert len(deltas) == len(coords)
+ n = len(deltas)
forced = set()
# Track "last" and "next" points on the contour as we sweep.
- nd, nc = delta[0], coords[0]
- ld, lc = delta[-1], coords[-1]
- for i in range(len(delta)-1, -1, -1):
- d, c = ld, lc
- ld, lc = delta[i-1], coords[i-1]
+ for i in range(len(deltas)-1, -1, -1):
+ ld, lc = deltas[i-1], coords[i-1]
+ d, c = deltas[i], coords[i]
+ nd, nc = deltas[i-n+1], coords[i-n+1]
for j in (0,1): # For X and for Y
cj = c[j]
@@ -128,42 +194,48 @@ def _iup_contour_bound_forced_set(delta, coords, tolerance=0):
c1, c2 = ncj, lcj
d1, d2 = ndj, ldj
+ force = False
+
+ # If the two coordinates are the same, then the interpolation
+ # algorithm produces the same delta if both deltas are equal,
+ # and zero if they differ.
+ #
+ # This test has to be before the next one.
+ if c1 == c2:
+ if abs(d1 - d2) > tolerance and abs(dj) > tolerance:
+ force = True
+
# If coordinate for current point is between coordinate of adjacent
# points on the two sides, but the delta for current point is NOT
# between delta for those adjacent points (considering tolerance
# allowance), then there is no way that current point can be IUP-ed.
# Mark it forced.
- force = False
- if c1 <= cj <= c2:
+ elif c1 <= cj <= c2: # and c1 != c2
if not (min(d1,d2)-tolerance <= dj <= max(d1,d2)+tolerance):
force = True
+
+ # Otherwise, the delta should either match the closest, or have the
+ # same sign as the interpolation of the two deltas.
else: # cj < c1 or c2 < cj
- if c1 == c2:
- if d1 == d2:
- if abs(dj - d1) > tolerance:
- force = True
- else:
- if abs(dj) > tolerance:
- # Disabled the following because the "d1 == d2" does
- # check does not take tolerance into consideration...
- pass # force = True
- elif d1 != d2:
+ if d1 != d2:
if cj < c1:
- if dj != d1 and ((dj-tolerance < d1) != (d1 < d2)):
+ if abs(dj) > tolerance and abs(dj - d1) > tolerance and ((dj-tolerance < d1) != (d1 < d2)):
force = True
else: # c2 < cj
- if d2 != dj and ((d2 < dj+tolerance) != (d1 < d2)):
+ if abs(dj) > tolerance and abs(dj - d2) > tolerance and ((d2 < dj+tolerance) != (d1 < d2)):
force = True
if force:
forced.add(i)
break
- nd, nc = d, c
-
return forced
-def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=None):
+def _iup_contour_optimize_dp(deltas : _DeltaSegment,
+ coords : _PointSegment,
+ forced={},
+ tolerance : Real = 0,
+ lookback : Integral =None):
"""Straightforward Dynamic-Programming. For each index i, find least-costly encoding of
points 0 to i where i is explicitly encoded. We find this by considering all previous
explicit points j and check whether interpolation can fill points between j and i.
@@ -173,9 +245,10 @@ def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=Non
As major speedup, we stop looking further whenever we see a "forced" point."""
- n = len(delta)
+ n = len(deltas)
if lookback is None:
lookback = n
+ lookback = min(lookback, MAX_LOOKBACK)
costs = {-1:0}
chain = {-1:None}
for i in range(0, n):
@@ -191,7 +264,7 @@ def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=Non
cost = costs[j] + 1
- if cost < best_cost and can_iup_in_between(delta, coords, j, i, tolerance):
+ if cost < best_cost and can_iup_in_between(deltas, coords, j, i, tolerance):
costs[i] = best_cost = cost
chain[i] = j
@@ -200,7 +273,7 @@ def _iup_contour_optimize_dp(delta, coords, forced={}, tolerance=0, lookback=Non
return chain, costs
-def _rot_list(l, k):
+def _rot_list(l : list, k : int):
"""Rotate list by k items forward. Ie. item at position 0 will be
at position k in returned list. Negative k is allowed."""
n = len(l)
@@ -208,48 +281,62 @@ def _rot_list(l, k):
if not k: return l
return l[n-k:] + l[:n-k]
-def _rot_set(s, k, n):
+def _rot_set(s : set, k : int, n : int):
k %= n
if not k: return s
return {(v + k) % n for v in s}
-def iup_contour_optimize(delta, coords, tolerance=0.):
- n = len(delta)
+def iup_contour_optimize(deltas : _DeltaSegment,
+ coords : _PointSegment,
+ tolerance : Real = 0.) -> _DeltaOrNoneSegment:
+ """For contour with coordinates `coords`, optimize a set of delta
+ values `deltas` within error `tolerance`.
+
+ Returns delta vector that has most number of None items instead of
+ the input delta.
+ """
+
+ n = len(deltas)
# Get the easy cases out of the way:
# If all are within tolerance distance of 0, encode nothing:
- if all(abs(complex(*p)) <= tolerance for p in delta):
+ if all(abs(complex(*p)) <= tolerance for p in deltas):
return [None] * n
# If there's exactly one point, return it:
if n == 1:
- return delta
+ return deltas
# If all deltas are exactly the same, return just one (the first one):
- d0 = delta[0]
- if all(d0 == d for d in delta):
+ d0 = deltas[0]
+ if all(d0 == d for d in deltas):
return [d0] + [None] * (n-1)
# Else, solve the general problem using Dynamic Programming.
- forced = _iup_contour_bound_forced_set(delta, coords, tolerance)
+ forced = _iup_contour_bound_forced_set(deltas, coords, tolerance)
# The _iup_contour_optimize_dp() routine returns the optimal encoding
# solution given the constraint that the last point is always encoded.
# To remove this constraint, we use two different methods, depending on
# whether forced set is non-empty or not:
+ # Debugging: Make the next if always take the second branch and observe
+ # if the font size changes (reduced); that would mean the forced-set
+ # has members it should not have.
if forced:
# Forced set is non-empty: rotate the contour start point
# such that the last point in the list is a forced point.
k = (n-1) - max(forced)
assert k >= 0
- delta = _rot_list(delta, k)
+ deltas = _rot_list(deltas, k)
coords = _rot_list(coords, k)
forced = _rot_set(forced, k, n)
- chain, costs = _iup_contour_optimize_dp(delta, coords, forced, tolerance)
+ # Debugging: Pass a set() instead of forced variable to the next call
+ # to exercise forced-set computation for under-counting.
+ chain, costs = _iup_contour_optimize_dp(deltas, coords, forced, tolerance)
# Assemble solution.
solution = set()
@@ -257,18 +344,25 @@ def iup_contour_optimize(delta, coords, tolerance=0.):
while i is not None:
solution.add(i)
i = chain[i]
+ solution.remove(-1)
+
+ #if not forced <= solution:
+ # print("coord", coords)
+ # print("deltas", deltas)
+ # print("len", len(deltas))
assert forced <= solution, (forced, solution)
- delta = [delta[i] if i in solution else None for i in range(n)]
- delta = _rot_list(delta, -k)
+ deltas = [deltas[i] if i in solution else None for i in range(n)]
+
+ deltas = _rot_list(deltas, -k)
else:
- # Repeat the contour an extra time, solve the 2*n case, then look for solutions of the
- # circular n-length problem in the solution for 2*n linear case. I cannot prove that
+ # Repeat the contour an extra time, solve the new case, then look for solutions of the
+ # circular n-length problem in the solution for new linear case. I cannot prove that
# this always produces the optimal solution...
- chain, costs = _iup_contour_optimize_dp(delta+delta, coords+coords, forced, tolerance, n)
+ chain, costs = _iup_contour_optimize_dp(deltas+deltas, coords+coords, forced, tolerance, n)
best_sol, best_cost = None, n+1
- for start in range(n-1, 2*n-1):
+ for start in range(n-1, len(costs) - 1):
# Assemble solution.
solution = set()
i = start
@@ -280,19 +374,35 @@ def iup_contour_optimize(delta, coords, tolerance=0.):
if cost <= best_cost:
best_sol, best_cost = solution, cost
- delta = [delta[i] if i in best_sol else None for i in range(n)]
+ #if not forced <= best_sol:
+ # print("coord", coords)
+ # print("deltas", deltas)
+ # print("len", len(deltas))
+ assert forced <= best_sol, (forced, best_sol)
+
+ deltas = [deltas[i] if i in best_sol else None for i in range(n)]
- return delta
+ return deltas
-def iup_delta_optimize(delta, coords, ends, tolerance=0.):
+def iup_delta_optimize(deltas : _DeltaSegment,
+ coords : _PointSegment,
+ ends : _Endpoints,
+ tolerance : Real = 0.) -> _DeltaOrNoneSegment:
+ """For the outline given in `coords`, with contour endpoints given
+ in sorted increasing order in `ends`, optimize a set of delta
+ values `deltas` within error `tolerance`.
+
+ Returns delta vector that has most number of None items instead of
+ the input delta.
+ """
assert sorted(ends) == ends and len(coords) == (ends[-1]+1 if ends else 0) + 4
n = len(coords)
ends = ends + [n-4, n-3, n-2, n-1]
out = []
start = 0
for end in ends:
- contour = iup_contour_optimize(delta[start:end+1], coords[start:end+1], tolerance)
+ contour = iup_contour_optimize(deltas[start:end+1], coords[start:end+1], tolerance)
assert len(contour) == end - start + 1
out.extend(contour)
start = end+1
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
index 3e5d2a9b..c9a1d3e3 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -3,15 +3,20 @@ Merge OpenType Layout tables (GDEF / GPOS / GSUB).
"""
import os
import copy
+import enum
from operator import ior
import logging
+from fontTools.colorLib.builder import MAX_PAINT_COLR_LAYER_COUNT, LayerReuseCache
from fontTools.misc import classifyTools
from fontTools.misc.roundTools import otRound
+from fontTools.misc.treeTools import build_n_ary_tree
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables import otBase as otBase
+from fontTools.ttLib.tables.otConverters import BaseFixedValue
+from fontTools.ttLib.tables.otTraverse import dfs_base_table
from fontTools.ttLib.tables.DefaultTable import DefaultTable
from fontTools.varLib import builder, models, varStore
-from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo
+from fontTools.varLib.models import nonNone, allNone, allEqual, allEqualTo, subList
from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
from fontTools.otlLib.builder import buildSinglePos
@@ -26,11 +31,12 @@ from .errors import (
ShouldBeConstant,
FoundANone,
MismatchedTypes,
+ NotANone,
LengthsDiffer,
KeysDiffer,
InconsistentGlyphOrder,
InconsistentExtensions,
- UnsupportedFormat,
+ InconsistentFormats,
UnsupportedFormat,
VarLibMergeError,
)
@@ -39,13 +45,15 @@ class Merger(object):
def __init__(self, font=None):
self.font = font
+ # mergeTables populates this from the parent's master ttfs
+ self.ttfs = None
@classmethod
def merger(celf, clazzes, attrs=(None,)):
assert celf != Merger, 'Subclass Merger instead.'
if 'mergers' not in celf.__dict__:
celf.mergers = {}
- if type(clazzes) == type:
+ if type(clazzes) in (type, enum.EnumMeta):
clazzes = (clazzes,)
if type(attrs) == str:
attrs = (attrs,)
@@ -81,10 +89,10 @@ class Merger(object):
def mergeObjects(self, out, lst, exclude=()):
if hasattr(out, "ensureDecompiled"):
- out.ensureDecompiled()
+ out.ensureDecompiled(recurse=False)
for item in lst:
if hasattr(item, "ensureDecompiled"):
- item.ensureDecompiled()
+ item.ensureDecompiled(recurse=False)
keys = sorted(vars(out).keys())
if not all(keys == sorted(vars(v).keys()) for v in lst):
raise KeysDiffer(self, expected=keys,
@@ -122,6 +130,11 @@ class Merger(object):
mergerFunc = self.mergersFor(out).get(None, None)
if mergerFunc is not None:
mergerFunc(self, out, lst)
+ elif isinstance(out, enum.Enum):
+ # need to special-case Enums as have __dict__ but are not regular 'objects',
+ # otherwise mergeObjects/mergeThings get trapped in a RecursionError
+ if not allEqualTo(out, lst):
+ raise ShouldBeConstant(self, expected=out, got=lst)
elif hasattr(out, '__dict__'):
self.mergeObjects(out, lst)
elif isinstance(out, list):
@@ -134,9 +147,8 @@ class Merger(object):
for tag in tableTags:
if tag not in font: continue
try:
- self.ttfs = [m for m in master_ttfs if tag in m]
- self.mergeThings(font[tag], [m[tag] if tag in m else None
- for m in master_ttfs])
+ self.ttfs = master_ttfs
+ self.mergeThings(font[tag], [m.get(tag) for m in master_ttfs])
except VarLibMergeError as e:
e.stack.append(tag)
raise
@@ -216,6 +228,20 @@ def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
for dict_set in dict_sets]
return order, padded
+@AligningMerger.merger(otBase.ValueRecord)
+def merge(merger, self, lst):
+ # Code below sometimes calls us with self being
+ # a new object. Copy it from lst and recurse.
+ self.__dict__ = lst[0].__dict__.copy()
+ merger.mergeObjects(self, lst)
+
+@AligningMerger.merger(ot.Anchor)
+def merge(merger, self, lst):
+ # Code below sometimes calls us with self being
+ # a new object. Copy it from lst and recurse.
+ self.__dict__ = lst[0].__dict__.copy()
+ merger.mergeObjects(self, lst)
+
def _Lookup_SinglePos_get_effective_value(merger, subtables, glyph):
for self in subtables:
if self is None or \
@@ -1036,11 +1062,19 @@ class VariationMerger(AligningMerger):
def mergeThings(self, out, lst):
masterModel = None
+ origTTFs = None
if None in lst:
if allNone(lst):
if out is not None:
raise FoundANone(self, got=lst)
return
+
+ # temporarily subset the list of master ttfs to the ones for which
+ # master values are not None
+ origTTFs = self.ttfs
+ if self.ttfs:
+ self.ttfs = subList([v is not None for v in lst], self.ttfs)
+
masterModel = self.model
model, lst = masterModel.getSubModel(lst)
self.setModel(model)
@@ -1049,6 +1083,8 @@ class VariationMerger(AligningMerger):
if masterModel:
self.setModel(masterModel)
+ if origTTFs:
+ self.ttfs = origTTFs
def buildVarDevTable(store_builder, master_values):
@@ -1099,3 +1135,408 @@ def merge(merger, self, lst):
setattr(self, name, value)
if deviceTable:
setattr(self, tableName, deviceTable)
+
+
+class COLRVariationMerger(VariationMerger):
+ """A specialized VariationMerger that takes multiple master fonts containing
+ COLRv1 tables, and builds a variable COLR font.
+
+ COLR tables are special in that variable subtables can be associated with
+ multiple delta-set indices (via VarIndexBase).
+ They also contain tables that must change their type (not simply the Format)
+ as they become variable (e.g. Affine2x3 -> VarAffine2x3) so this merger takes
+ care of that too.
+ """
+
+ def __init__(self, model, axisTags, font, allowLayerReuse=True):
+ VariationMerger.__init__(self, model, axisTags, font)
+ # maps {tuple(varIdxes): VarIndexBase} to facilitate reuse of VarIndexBase
+ # between variable tables with same varIdxes.
+ self.varIndexCache = {}
+ # flat list of all the varIdxes generated while merging
+ self.varIdxes = []
+ # set of id()s of the subtables that contain variations after merging
+ # and need to be upgraded to the associated VarType.
+ self.varTableIds = set()
+ # we keep these around for rebuilding a LayerList while merging PaintColrLayers
+ self.layers = []
+ self.layerReuseCache = None
+ if allowLayerReuse:
+ self.layerReuseCache = LayerReuseCache()
+ # flag to ensure BaseGlyphList is fully merged before LayerList gets processed
+ self._doneBaseGlyphs = False
+
+ def mergeTables(self, font, master_ttfs, tableTags=("COLR",)):
+ if "COLR" in tableTags and "COLR" in font:
+ # The merger modifies the destination COLR table in-place. If this contains
+ # multiple PaintColrLayers referencing the same layers from LayerList, it's
+ # a problem because we may risk modifying the same paint more than once, or
+ # worse, fail while attempting to do that.
+ # We don't know whether the master COLR table was built with layer reuse
+ # disabled, thus to be safe we rebuild its LayerList so that it contains only
+ # unique layers referenced from non-overlapping PaintColrLayers throughout
+ # the base paint graphs.
+ self.expandPaintColrLayers(font["COLR"].table)
+ VariationMerger.mergeTables(self, font, master_ttfs, tableTags)
+
+ def checkFormatEnum(self, out, lst, validate=lambda _: True):
+ fmt = out.Format
+ formatEnum = out.formatEnum
+ ok = False
+ try:
+ fmt = formatEnum(fmt)
+ except ValueError:
+ pass
+ else:
+ ok = validate(fmt)
+ if not ok:
+ raise UnsupportedFormat(
+ self, subtable=type(out).__name__, value=fmt
+ )
+ expected = fmt
+ got = []
+ for v in lst:
+ fmt = getattr(v, "Format", None)
+ try:
+ fmt = formatEnum(fmt)
+ except ValueError:
+ pass
+ got.append(fmt)
+ if not allEqualTo(expected, got):
+ raise InconsistentFormats(
+ self,
+ subtable=type(out).__name__,
+ expected=expected,
+ got=got,
+ )
+ return expected
+
+ def mergeSparseDict(self, out, lst):
+ for k in out.keys():
+ try:
+ self.mergeThings(out[k], [v.get(k) for v in lst])
+ except VarLibMergeError as e:
+ e.stack.append(f"[{k!r}]")
+ raise
+
+ def mergeAttrs(self, out, lst, attrs):
+ for attr in attrs:
+ value = getattr(out, attr)
+ values = [getattr(item, attr) for item in lst]
+ try:
+ self.mergeThings(value, values)
+ except VarLibMergeError as e:
+ e.stack.append(f".{attr}")
+ raise
+
+ def storeMastersForAttr(self, out, lst, attr):
+ master_values = [getattr(item, attr) for item in lst]
+
+ # VarStore treats deltas for fixed-size floats as integers, so we
+ # must convert master values to int before storing them in the builder
+ # then back to float.
+ is_fixed_size_float = False
+ conv = out.getConverterByName(attr)
+ if isinstance(conv, BaseFixedValue):
+ is_fixed_size_float = True
+ master_values = [conv.toInt(v) for v in master_values]
+
+ baseValue = master_values[0]
+ varIdx = ot.NO_VARIATION_INDEX
+ if not allEqual(master_values):
+ baseValue, varIdx = self.store_builder.storeMasters(master_values)
+
+ if is_fixed_size_float:
+ baseValue = conv.fromInt(baseValue)
+
+ return baseValue, varIdx
+
+ def storeVariationIndices(self, varIdxes) -> int:
+ # try to reuse an existing VarIndexBase for the same varIdxes, or else
+ # create a new one
+ key = tuple(varIdxes)
+ varIndexBase = self.varIndexCache.get(key)
+
+ if varIndexBase is None:
+ # scan for a full match anywhere in the self.varIdxes
+ for i in range(len(self.varIdxes) - len(varIdxes) + 1):
+ if self.varIdxes[i:i+len(varIdxes)] == varIdxes:
+ self.varIndexCache[key] = varIndexBase = i
+ break
+
+ if varIndexBase is None:
+ # try find a partial match at the end of the self.varIdxes
+ for n in range(len(varIdxes)-1, 0, -1):
+ if self.varIdxes[-n:] == varIdxes[:n]:
+ varIndexBase = len(self.varIdxes) - n
+ self.varIndexCache[key] = varIndexBase
+ self.varIdxes.extend(varIdxes[n:])
+ break
+
+ if varIndexBase is None:
+ # no match found, append at the end
+ self.varIndexCache[key] = varIndexBase = len(self.varIdxes)
+ self.varIdxes.extend(varIdxes)
+
+ return varIndexBase
+
+ def mergeVariableAttrs(self, out, lst, attrs) -> int:
+ varIndexBase = ot.NO_VARIATION_INDEX
+ varIdxes = []
+ for attr in attrs:
+ baseValue, varIdx = self.storeMastersForAttr(out, lst, attr)
+ setattr(out, attr, baseValue)
+ varIdxes.append(varIdx)
+
+ if any(v != ot.NO_VARIATION_INDEX for v in varIdxes):
+ varIndexBase = self.storeVariationIndices(varIdxes)
+
+ return varIndexBase
+
+ @classmethod
+ def convertSubTablesToVarType(cls, table):
+ for path in dfs_base_table(
+ table,
+ skip_root=True,
+ predicate=lambda path: (
+ getattr(type(path[-1].value), "VarType", None) is not None
+ )
+ ):
+ st = path[-1]
+ subTable = st.value
+ varType = type(subTable).VarType
+ newSubTable = varType()
+ newSubTable.__dict__.update(subTable.__dict__)
+ newSubTable.populateDefaults()
+ parent = path[-2].value
+ if st.index is not None:
+ getattr(parent, st.name)[st.index] = newSubTable
+ else:
+ setattr(parent, st.name, newSubTable)
+
+ @staticmethod
+ def expandPaintColrLayers(colr):
+ """Rebuild LayerList without PaintColrLayers reuse.
+
+ Each base paint graph is fully DFS-traversed (with exception of PaintColrGlyph
+ which are irrelevant for this); any layers referenced via PaintColrLayers are
+ collected into a new LayerList and duplicated when reuse is detected, to ensure
+ that all paints are distinct objects at the end of the process.
+ PaintColrLayers's FirstLayerIndex/NumLayers are updated so that no overlap
+ is left. Also, any consecutively nested PaintColrLayers are flattened.
+ The COLR table's LayerList is replaced with the new unique layers.
+ A side effect is also that any layer from the old LayerList which is not
+ referenced by any PaintColrLayers is dropped.
+ """
+ if not colr.LayerList:
+ # if no LayerList, there's nothing to expand
+ return
+ uniqueLayerIDs = set()
+ newLayerList = []
+ for rec in colr.BaseGlyphList.BaseGlyphPaintRecord:
+ frontier = [rec.Paint]
+ while frontier:
+ paint = frontier.pop()
+ if paint.Format == ot.PaintFormat.PaintColrGlyph:
+ # don't traverse these, we treat them as constant for merging
+ continue
+ elif paint.Format == ot.PaintFormat.PaintColrLayers:
+ # de-treeify any nested PaintColrLayers, append unique copies to
+ # the new layer list and update PaintColrLayers index/count
+ children = list(_flatten_layers(paint, colr))
+ first_layer_index = len(newLayerList)
+ for layer in children:
+ if id(layer) in uniqueLayerIDs:
+ layer = copy.deepcopy(layer)
+ assert id(layer) not in uniqueLayerIDs
+ newLayerList.append(layer)
+ uniqueLayerIDs.add(id(layer))
+ paint.FirstLayerIndex = first_layer_index
+ paint.NumLayers = len(children)
+ else:
+ children = paint.getChildren(colr)
+ frontier.extend(reversed(children))
+ # sanity check all the new layers are distinct objects
+ assert len(newLayerList) == len(uniqueLayerIDs)
+ colr.LayerList.Paint = newLayerList
+ colr.LayerList.LayerCount = len(newLayerList)
+
+
+@COLRVariationMerger.merger(ot.BaseGlyphList)
+def merge(merger, self, lst):
+ # ignore BaseGlyphCount, allow sparse glyph sets across masters
+ out = {rec.BaseGlyph: rec for rec in self.BaseGlyphPaintRecord}
+ masters = [{rec.BaseGlyph: rec for rec in m.BaseGlyphPaintRecord} for m in lst]
+
+ for i, g in enumerate(out.keys()):
+ try:
+ # missing base glyphs don't participate in the merge
+ merger.mergeThings(out[g], [v.get(g) for v in masters])
+ except VarLibMergeError as e:
+ e.stack.append(f".BaseGlyphPaintRecord[{i}]")
+ e.cause["location"] = f"base glyph {g!r}"
+ raise
+
+ merger._doneBaseGlyphs = True
+
+
+@COLRVariationMerger.merger(ot.LayerList)
+def merge(merger, self, lst):
+ # nothing to merge for LayerList, assuming we have already merged all PaintColrLayers
+ # found while traversing the paint graphs rooted at BaseGlyphPaintRecords.
+ assert merger._doneBaseGlyphs, "BaseGlyphList must be merged before LayerList"
+ # Simply flush the final list of layers and go home.
+ self.LayerCount = len(merger.layers)
+ self.Paint = merger.layers
+
+
+def _flatten_layers(root, colr):
+ assert root.Format == ot.PaintFormat.PaintColrLayers
+ for paint in root.getChildren(colr):
+ if paint.Format == ot.PaintFormat.PaintColrLayers:
+ yield from _flatten_layers(paint, colr)
+ else:
+ yield paint
+
+
+def _merge_PaintColrLayers(self, out, lst):
+ # we only enforce that the (flat) number of layers is the same across all masters
+ # but we allow FirstLayerIndex to differ to acommodate for sparse glyph sets.
+
+ out_layers = list(_flatten_layers(out, self.font["COLR"].table))
+
+ # sanity check ttfs are subset to current values (see VariationMerger.mergeThings)
+ # before matching each master PaintColrLayers to its respective COLR by position
+ assert len(self.ttfs) == len(lst)
+ master_layerses = [
+ list(_flatten_layers(lst[i], self.ttfs[i]["COLR"].table))
+ for i in range(len(lst))
+ ]
+
+ try:
+ self.mergeLists(out_layers, master_layerses)
+ except VarLibMergeError as e:
+ # NOTE: This attribute doesn't actually exist in PaintColrLayers but it's
+ # handy to have it in the stack trace for debugging.
+ e.stack.append(".Layers")
+ raise
+
+ # following block is very similar to LayerListBuilder._beforeBuildPaintColrLayers
+ # but I couldn't find a nice way to share the code between the two...
+
+ if self.layerReuseCache is not None:
+ # successful reuse can make the list smaller
+ out_layers = self.layerReuseCache.try_reuse(out_layers)
+
+ # if the list is still too big we need to tree-fy it
+ is_tree = len(out_layers) > MAX_PAINT_COLR_LAYER_COUNT
+ out_layers = build_n_ary_tree(out_layers, n=MAX_PAINT_COLR_LAYER_COUNT)
+
+ # We now have a tree of sequences with Paint leaves.
+ # Convert the sequences into PaintColrLayers.
+ def listToColrLayers(paint):
+ if isinstance(paint, list):
+ layers = [listToColrLayers(l) for l in paint]
+ paint = ot.Paint()
+ paint.Format = int(ot.PaintFormat.PaintColrLayers)
+ paint.NumLayers = len(layers)
+ paint.FirstLayerIndex = len(self.layers)
+ self.layers.extend(layers)
+ if self.layerReuseCache is not None:
+ self.layerReuseCache.add(layers, paint.FirstLayerIndex)
+ return paint
+
+ out_layers = [listToColrLayers(l) for l in out_layers]
+
+ if len(out_layers) == 1 and out_layers[0].Format == ot.PaintFormat.PaintColrLayers:
+ # special case when the reuse cache finds a single perfect PaintColrLayers match
+ # (it can only come from a successful reuse, _flatten_layers has gotten rid of
+ # all nested PaintColrLayers already); we assign it directly and avoid creating
+ # an extra table
+ out.NumLayers = out_layers[0].NumLayers
+ out.FirstLayerIndex = out_layers[0].FirstLayerIndex
+ else:
+ out.NumLayers = len(out_layers)
+ out.FirstLayerIndex = len(self.layers)
+
+ self.layers.extend(out_layers)
+
+ # Register our parts for reuse provided we aren't a tree
+ # If we are a tree the leaves registered for reuse and that will suffice
+ if self.layerReuseCache is not None and not is_tree:
+ self.layerReuseCache.add(out_layers, out.FirstLayerIndex)
+
+
+@COLRVariationMerger.merger((ot.Paint, ot.ClipBox))
+def merge(merger, self, lst):
+ fmt = merger.checkFormatEnum(self, lst, lambda fmt: not fmt.is_variable())
+
+ if fmt is ot.PaintFormat.PaintColrLayers:
+ _merge_PaintColrLayers(merger, self, lst)
+ return
+
+ varFormat = fmt.as_variable()
+
+ varAttrs = ()
+ if varFormat is not None:
+ varAttrs = otBase.getVariableAttrs(type(self), varFormat)
+ staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
+
+ merger.mergeAttrs(self, lst, staticAttrs)
+
+ varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
+
+ subTables = [st.value for st in self.iterSubTables()]
+
+ # Convert table to variable if itself has variations or any subtables have
+ isVariable = (
+ varIndexBase != ot.NO_VARIATION_INDEX
+ or any(id(table) in merger.varTableIds for table in subTables)
+ )
+
+ if isVariable:
+ if varAttrs:
+ # Some PaintVar* don't have any scalar attributes that can vary,
+ # only indirect offsets to other variable subtables, thus have
+ # no VarIndexBase of their own (e.g. PaintVarTransform)
+ self.VarIndexBase = varIndexBase
+
+ if subTables:
+ # Convert Affine2x3 -> VarAffine2x3, ColorLine -> VarColorLine, etc.
+ merger.convertSubTablesToVarType(self)
+
+ assert varFormat is not None
+ self.Format = int(varFormat)
+
+
+@COLRVariationMerger.merger((ot.Affine2x3, ot.ColorStop))
+def merge(merger, self, lst):
+ varType = type(self).VarType
+
+ varAttrs = otBase.getVariableAttrs(varType)
+ staticAttrs = (c.name for c in self.getConverters() if c.name not in varAttrs)
+
+ merger.mergeAttrs(self, lst, staticAttrs)
+
+ varIndexBase = merger.mergeVariableAttrs(self, lst, varAttrs)
+
+ if varIndexBase != ot.NO_VARIATION_INDEX:
+ self.VarIndexBase = varIndexBase
+ # mark as having variations so the parent table will convert to Var{Type}
+ merger.varTableIds.add(id(self))
+
+
+@COLRVariationMerger.merger(ot.ColorLine)
+def merge(merger, self, lst):
+ merger.mergeAttrs(self, lst, (c.name for c in self.getConverters()))
+
+ if any(id(stop) in merger.varTableIds for stop in self.ColorStop):
+ merger.convertSubTablesToVarType(self)
+ merger.varTableIds.add(id(self))
+
+
+@COLRVariationMerger.merger(ot.ClipList, "clips")
+def merge(merger, self, lst):
+ # 'sparse' in that we allow non-default masters to omit ClipBox entries
+ # for some/all glyphs (i.e. they don't participate)
+ merger.mergeSparseDict(self, lst)
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py
index c548fbca..a7e020b0 100644
--- a/Lib/fontTools/varLib/models.py
+++ b/Lib/fontTools/varLib/models.py
@@ -1,11 +1,6 @@
"""Variation fonts interpolation models."""
__all__ = [
- "nonNone",
- "allNone",
- "allEqual",
- "allEqualTo",
- "subList",
"normalizeValue",
"normalizeLocation",
"supportScalar",
@@ -50,12 +45,13 @@ def subList(truth, lst):
def normalizeValue(v, triple):
"""Normalizes value based on a min/default/max triple.
- >>> normalizeValue(400, (100, 400, 900))
- 0.0
- >>> normalizeValue(100, (100, 400, 900))
- -1.0
- >>> normalizeValue(650, (100, 400, 900))
- 0.5
+
+ >>> normalizeValue(400, (100, 400, 900))
+ 0.0
+ >>> normalizeValue(100, (100, 400, 900))
+ -1.0
+ >>> normalizeValue(650, (100, 400, 900))
+ 0.5
"""
lower, default, upper = triple
if not (lower <= default <= upper):
@@ -75,41 +71,42 @@ def normalizeValue(v, triple):
def normalizeLocation(location, axes):
"""Normalizes location based on axis min/default/max values from axes.
- >>> axes = {"wght": (100, 400, 900)}
- >>> normalizeLocation({"wght": 400}, axes)
- {'wght': 0.0}
- >>> normalizeLocation({"wght": 100}, axes)
- {'wght': -1.0}
- >>> normalizeLocation({"wght": 900}, axes)
- {'wght': 1.0}
- >>> normalizeLocation({"wght": 650}, axes)
- {'wght': 0.5}
- >>> normalizeLocation({"wght": 1000}, axes)
- {'wght': 1.0}
- >>> normalizeLocation({"wght": 0}, axes)
- {'wght': -1.0}
- >>> axes = {"wght": (0, 0, 1000)}
- >>> normalizeLocation({"wght": 0}, axes)
- {'wght': 0.0}
- >>> normalizeLocation({"wght": -1}, axes)
- {'wght': 0.0}
- >>> normalizeLocation({"wght": 1000}, axes)
- {'wght': 1.0}
- >>> normalizeLocation({"wght": 500}, axes)
- {'wght': 0.5}
- >>> normalizeLocation({"wght": 1001}, axes)
- {'wght': 1.0}
- >>> axes = {"wght": (0, 1000, 1000)}
- >>> normalizeLocation({"wght": 0}, axes)
- {'wght': -1.0}
- >>> normalizeLocation({"wght": -1}, axes)
- {'wght': -1.0}
- >>> normalizeLocation({"wght": 500}, axes)
- {'wght': -0.5}
- >>> normalizeLocation({"wght": 1000}, axes)
- {'wght': 0.0}
- >>> normalizeLocation({"wght": 1001}, axes)
- {'wght': 0.0}
+
+ >>> axes = {"wght": (100, 400, 900)}
+ >>> normalizeLocation({"wght": 400}, axes)
+ {'wght': 0.0}
+ >>> normalizeLocation({"wght": 100}, axes)
+ {'wght': -1.0}
+ >>> normalizeLocation({"wght": 900}, axes)
+ {'wght': 1.0}
+ >>> normalizeLocation({"wght": 650}, axes)
+ {'wght': 0.5}
+ >>> normalizeLocation({"wght": 1000}, axes)
+ {'wght': 1.0}
+ >>> normalizeLocation({"wght": 0}, axes)
+ {'wght': -1.0}
+ >>> axes = {"wght": (0, 0, 1000)}
+ >>> normalizeLocation({"wght": 0}, axes)
+ {'wght': 0.0}
+ >>> normalizeLocation({"wght": -1}, axes)
+ {'wght': 0.0}
+ >>> normalizeLocation({"wght": 1000}, axes)
+ {'wght': 1.0}
+ >>> normalizeLocation({"wght": 500}, axes)
+ {'wght': 0.5}
+ >>> normalizeLocation({"wght": 1001}, axes)
+ {'wght': 1.0}
+ >>> axes = {"wght": (0, 1000, 1000)}
+ >>> normalizeLocation({"wght": 0}, axes)
+ {'wght': -1.0}
+ >>> normalizeLocation({"wght": -1}, axes)
+ {'wght': -1.0}
+ >>> normalizeLocation({"wght": 500}, axes)
+ {'wght': -0.5}
+ >>> normalizeLocation({"wght": 1000}, axes)
+ {'wght': 0.0}
+ >>> normalizeLocation({"wght": 1001}, axes)
+ {'wght': 0.0}
"""
out = {}
for tag, triple in axes.items():
@@ -118,27 +115,32 @@ def normalizeLocation(location, axes):
return out
-def supportScalar(location, support, ot=True):
+def supportScalar(location, support, ot=True, extrapolate=False):
"""Returns the scalar multiplier at location, for a master
with support. If ot is True, then a peak value of zero
for support of an axis means "axis does not participate". That
is how OpenType Variation Font technology works.
- >>> supportScalar({}, {})
- 1.0
- >>> supportScalar({'wght':.2}, {})
- 1.0
- >>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
- 0.1
- >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
- 0.75
- >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
- 0.75
- >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False)
- 0.375
- >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
- 0.75
- >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
- 0.75
+
+ >>> supportScalar({}, {})
+ 1.0
+ >>> supportScalar({'wght':.2}, {})
+ 1.0
+ >>> supportScalar({'wght':.2}, {'wght':(0,2,3)})
+ 0.1
+ >>> supportScalar({'wght':2.5}, {'wght':(0,2,4)})
+ 0.75
+ >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
+ 0.75
+ >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)}, ot=False)
+ 0.375
+ >>> supportScalar({'wght':2.5, 'wdth':0}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
+ 0.75
+ >>> supportScalar({'wght':2.5, 'wdth':.5}, {'wght':(0,2,4), 'wdth':(-1,0,+1)})
+ 0.75
+ >>> supportScalar({'wght':4}, {'wght':(0,2,3)}, extrapolate=True)
+ 2.0
+ >>> supportScalar({'wght':4}, {'wght':(0,2,2)}, extrapolate=True)
+ 2.0
"""
scalar = 1.0
for axis, (lower, peak, upper) in support.items():
@@ -156,9 +158,27 @@ def supportScalar(location, support, ot=True):
v = location[axis]
if v == peak:
continue
+
+ if extrapolate:
+ if v < -1 and lower <= -1:
+ if peak <= -1 and peak < upper:
+ scalar *= (v - upper) / (peak - upper)
+ continue
+ elif -1 < peak:
+ scalar *= (v - lower) / (peak - lower)
+ continue
+ elif +1 < v and +1 <= upper:
+ if +1 <= peak and lower < peak:
+ scalar *= (v - lower) / (peak - lower)
+ continue
+ elif peak < +1:
+ scalar *= (v - upper) / (peak - upper)
+ continue
+
if v <= lower or upper <= v:
scalar = 0.0
break
+
if v < peak:
scalar *= (v - lower) / (peak - lower)
else: # v > peak
@@ -167,10 +187,11 @@ def supportScalar(location, support, ot=True):
class VariationModel(object):
+ """Locations must have the base master at the origin (ie. 0).
- """
- Locations must be in normalized space. Ie. base master
- is at origin (0)::
+ If the extrapolate argument is set to True, then location values are
+ interpretted in the normalized space, ie. in the [-1,+1] range, and
+ values are extrapolated outside this range.
>>> from pprint import pprint
>>> locations = [ \
@@ -210,14 +231,16 @@ class VariationModel(object):
5: 0.6666666666666667,
6: 0.4444444444444445,
7: 0.6666666666666667}]
- """
+ """
+
+ def __init__(self, locations, axisOrder=None, extrapolate=False):
- def __init__(self, locations, axisOrder=None):
if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
raise VariationModelError("Locations must be unique.")
self.origLocations = locations
self.axisOrder = axisOrder if axisOrder is not None else []
+ self.extrapolate = extrapolate
locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
keyFunc = self.getMasterLocationsSortKeyFunc(
@@ -416,7 +439,8 @@ class VariationModel(object):
return model.getDeltas(items, round=round), model.supports
def getScalars(self, loc):
- return [supportScalar(loc, support) for support in self.supports]
+ return [supportScalar(loc, support, extrapolate=self.extrapolate)
+ for support in self.supports]
@staticmethod
def interpolateFromDeltasAndScalars(deltas, scalars):
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
index 263c4e61..2e674798 100644
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -412,6 +412,9 @@ def main(args=None):
parser.add_argument(
"-o", "--output", metavar="OUTPUT.ttf", default=None,
help="Output instance TTF file (default: INPUT-instance.ttf).")
+ parser.add_argument(
+ "--no-recalc-timestamp", dest="recalc_timestamp", action='store_false',
+ help="Don't set the output font's timestamp to the current time.")
logging_group = parser.add_mutually_exclusive_group(required=False)
logging_group.add_argument(
"-v", "--verbose", action="store_true", help="Run more verbosely.")
@@ -445,7 +448,7 @@ def main(args=None):
log.info("Location: %s", loc)
log.info("Loading variable font")
- varfont = TTFont(varfilename)
+ varfont = TTFont(varfilename, recalcTimestamp=options.recalc_timestamp)
instantiateVariableFont(varfont, loc, inplace=True, overlap=options.overlap)
diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py
index bcf81b39..2ffc6b13 100644
--- a/Lib/fontTools/varLib/varStore.py
+++ b/Lib/fontTools/varLib/varStore.py
@@ -7,6 +7,10 @@ from functools import partial
from collections import defaultdict
+NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX
+ot.VarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX
+
+
def _getLocationKey(loc):
return tuple(sorted(loc.items(), key=lambda kv: kv[0]))
@@ -135,6 +139,11 @@ def VarRegion_get_support(self, fvar_axes):
ot.VarRegion.get_support = VarRegion_get_support
+def VarStore___bool__(self):
+ return bool(self.VarData)
+
+ot.VarStore.__bool__ = VarStore___bool__
+
class VarStoreInstancer(object):
def __init__(self, varstore, fvar_axes, location={}):
@@ -169,6 +178,7 @@ class VarStoreInstancer(object):
def __getitem__(self, varidx):
major, minor = varidx >> 16, varidx & 0xFFFF
+ if varidx == NO_VARIATION_INDEX: return 0.
varData = self._varData
scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
deltas = varData[major].Item[minor]
@@ -192,6 +202,8 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True, retainFirstMap=False
# Sort out used varIdxes by major/minor.
used = {}
for varIdx in varIdxes:
+ if varIdx == NO_VARIATION_INDEX:
+ continue
major = varIdx >> 16
minor = varIdx & 0xFFFF
d = used.get(major)
@@ -206,7 +218,7 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True, retainFirstMap=False
varData = self.VarData
newVarData = []
- varDataMap = {}
+ varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX}
for major,data in enumerate(varData):
usedMinors = used.get(major)
if usedMinors is None:
@@ -431,7 +443,7 @@ class _EncodingDict(dict):
return chars
-def VarStore_optimize(self):
+def VarStore_optimize(self, use_NO_VARIATION_INDEX=True):
"""Optimize storage. Returns mapping from old VarIdxes to new ones."""
# TODO
@@ -455,6 +467,10 @@ def VarStore_optimize(self):
row[regionIdx] += v
row = tuple(row)
+ if use_NO_VARIATION_INDEX and not any(row):
+ front_mapping[(major<<16)+minor] = None
+ continue
+
encodings.add_row(row)
front_mapping[(major<<16)+minor] = row
@@ -537,9 +553,9 @@ def VarStore_optimize(self):
back_mapping[item] = (major<<16)+minor
# Compile final mapping.
- varidx_map = {}
+ varidx_map = {NO_VARIATION_INDEX:NO_VARIATION_INDEX}
for k,v in front_mapping.items():
- varidx_map[k] = back_mapping[v]
+ varidx_map[k] = back_mapping[v] if v is not None else NO_VARIATION_INDEX
# Remove unused regions.
self.prune_regions()
diff --git a/METADATA b/METADATA
index 71fb8a6e..7ab3b753 100644
--- a/METADATA
+++ b/METADATA
@@ -10,14 +10,14 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/archive/4.33.3.zip"
+ value: "https://github.com/fonttools/fonttools/archive/4.37.1.zip"
}
- version: "4.33.3"
+ version: "4.37.1"
license_type: BY_EXCEPTION_ONLY
license_note: "contains OFL fonts"
last_upgrade_date {
year: 2022
- month: 5
- day: 12
+ month: 8
+ day: 24
}
}
diff --git a/NEWS.rst b/NEWS.rst
index d500dd95..f022ad23 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,152 @@
+4.37.1 (released 2022-08-24)
+----------------------------
+
+- [subset] Fixed regression introduced with v4.37.0 while subsetting the VarStore of
+ ``HVAR`` and ``VVAR`` tables, whereby an ``AttributeError: subset_varidxes`` was
+ thrown because an apparently unused import statement (with the side-effect of
+ dynamically binding that ``subset_varidxes`` method to the VarStore class) had been
+ accidentally deleted in an unrelated PR (#2679, #2773).
+- [pens] Added ``cairoPen`` (#2678).
+- [gvar] Read ``gvar`` more lazily by not parsing all of the ``glyf`` table (#2771).
+- [ttGlyphSet] Make ``drawPoints(pointPen)`` method work for CFF fonts as well via
+ adapter pen (#2770).
+
+4.37.0 (released 2022-08-23)
+----------------------------
+
+- [varLib.models] Reverted PR #2717 which added support for "narrow tents" in v4.36.0,
+ as it introduced a regression (#2764, #2765). It will be restored in upcoming release
+ once we found a solution to the bug.
+- [cff.specializer] Fixed issue in charstring generalizer with the ``blend`` operator
+ (#2750, #1975).
+- [varLib.models] Added support for extrapolation (#2757).
+- [ttGlyphSet] Ensure the newly added ``_TTVarGlyphSet`` inherits from ``_TTGlyphSet``
+ to keep backward compatibility with existing API (#2762).
+- [kern] Allow compiling legacy kern tables with more than 64k entries (d21cfdede).
+- [visitor] Added new visitor API to traverse tree of objects and dispatch based
+ on the attribute type: cf. ``fontTools.misc.visitor`` and ``fontTools.ttLib.ttVisitor``. Added ``fontTools.ttLib.scaleUpem`` module that uses the latter to
+ change a font's units-per-em and scale all the related fields accordingly (#2718,
+ #2755).
+
+4.36.0 (released 2022-08-17)
+----------------------------
+
+- [varLib.models] Use a simpler model that generates narrower "tents" (regions, master
+ supports) whenever possible: specifically when any two axes that actively "cooperate"
+ (have masters at non-zero positions for both axes) have a complete set of intermediates.
+ The simpler algorithm produces fewer overlapping regions and behaves better with
+ respect to rounding at the peak positions than the generic solver, always matching
+ intermediate masters exactly, instead of maximally 0.5 units off. This may be useful
+ when 100% metrics compatibility is desired (#2218, #2717).
+- [feaLib] Remove warning when about ``GDEF`` not being built when explicitly not
+ requested; don't build one unconditonally even when not requested (#2744, also works
+ around #2747).
+- [ttFont] ``TTFont.getGlyphSet`` method now supports selecting a location that
+ represents an instance of a variable font (supports both user-scale and normalized
+ axes coordinates via the ``normalized=False`` parameter). Currently this only works
+ for TrueType-flavored variable fonts (#2738).
+
+4.35.0 (released 2022-08-15)
+----------------------------
+
+- [otData/otConverters] Added support for 'biased' PaintSweepGradient start/end angles
+ to match latest COLRv1 spec (#2743).
+- [varLib.instancer] Fixed bug in ``_instantiateFeatureVariations`` when at the same
+ time pinning one axis and restricting the range of a subsequent axis; the wrong axis
+ tag was being used in the latter step (as the records' axisIdx was updated in the
+ preceding step but looked up using the old axes order in the following step) (#2733,
+ #2734).
+- [mtiLib] Pad script tags with space when less than 4 char long (#1727).
+- [merge] Use ``'.'`` instead of ``'#'`` in duplicate glyph names (#2742).
+- [gvar] Added support for lazily loading glyph variations (#2741).
+- [varLib] In ``build_many``, we forgot to pass on ``colr_layer_reuse`` parameter to
+ the ``build`` method (#2730).
+- [svgPathPen] Add a main that prints SVG for input text (6df779fd).
+- [cffLib.width] Fixed off-by-one in optimized values; previous code didn't match the
+ code block above it (2963fa50).
+- [varLib.interpolatable] Support reading .designspace and .glyphs files (via optional
+ ``glyphsLib``).
+- Compile some modules with Cython when available and building/installing fonttools
+ from source: ``varLib.iup`` (35% faster), ``pens.momentsPen`` (makes
+ ``varLib.interpolatable`` 3x faster).
+- [feaLib] Allow features to be built for VF without also building a GDEF table (e.g.
+ only build GSUB); warn when GDEF would be needed but isn't requested (#2705, 2694).
+- [otBase] Fixed ``AttributeError`` when uharfbuzz < 0.23.0 and 'repack' method is
+ missing (32aa8eaf). Use new ``uharfbuzz.repack_with_tag`` when available (since
+ uharfbuzz>=0.30.0), enables table-specific optimizations to be performed during
+ repacking (#2724).
+- [statisticsPen] By default report all glyphs (4139d891). Avoid division-by-zero
+ (52b28f90).
+- [feaLib] Added missing required argument to FeatureLibError exception (#2693)
+- [varLib.merge] Fixed error during error reporting (#2689). Fixed undefined
+ ``NotANone`` variable (#2714).
+
+4.34.4 (released 2022-07-07)
+----------------------------
+
+- Fixed typo in varLib/merger.py that causes NameError merging COLR glyphs
+ containing more than 255 layers (#2685).
+
+4.34.3 (released 2022-07-07)
+----------------------------
+
+- [designspaceLib] Don't make up bad PS names when no STAT data (#2684)
+
+4.34.2 (released 2022-07-06)
+----------------------------
+
+- [varStore/subset] fixed KeyError exception to do with NO_VARIATION_INDEX while
+ subsetting varidxes in GPOS/GDEF (a08140d).
+
+4.34.1 (released 2022-07-06)
+----------------------------
+
+- [instancer] When optimizing HVAR/VVAR VarStore, use_NO_VARIATION_INDEX=False to avoid
+ including NO_VARIATION_INDEX in AdvWidthMap, RsbMap, LsbMap mappings, which would
+ push the VarIdx width to maximum (4bytes), which is not desirable. This also fixes
+ a hard crash when attempting to subset a varfont after it had been partially instanced
+ with use_NO_VARIATION_INDEX=True.
+
+4.34.0 (released 2022-07-06)
+----------------------------
+
+- [instancer] Set RIBBI bits in head and OS/2 table when cutting instances and the
+ subfamily nameID=2 contains strings like 'Italic' or 'Bold' (#2673).
+- [otTraverse] Addded module containing methods for traversing trees of otData tables
+ (#2660).
+- [otTables] Made DeltaSetIndexMap TTX dump less verbose by omitting no-op entries
+ (#2660).
+- [colorLib.builder] Added option to disable PaintColrLayers's reuse of layers from
+ LayerList (#2660).
+- [varLib] Added support for merging multiple master COLRv1 tables into a variable
+ COLR table (#2660, #2328). Base color glyphs of same name in different masters must have
+ identical paint graph structure (incl. number of layers, palette indices, number
+ of color line stops, corresponding paint formats at each level of the graph),
+ but can differ in the variable fields (e.g. PaintSolid.Alpha). PaintVar* tables
+ are produced when this happens and a VarStore/DeltaSetIndexMap is added to the
+ variable COLR table. It is possible for non-default masters to be 'sparse', i.e.
+ omit some of the color glyphs present in the default master.
+- [feaLib] Let the Parser set nameIDs 1 through 6 that were previously reserved (#2675).
+- [varLib.varStore] Support NO_VARIATION_INDEX in optimizer and instancer.
+- [feaLib] Show all missing glyphs at once at end of parsing (#2665).
+- [varLib.iup] Rewrite force-set conditions and limit DP loopback length (#2651).
+ For Noto Sans, IUP time drops from 23s down to 9s, with only a slight size increase
+ in the final font. This basically turns the algorithm from O(n^3) into O(n).
+- [featureVars] Report about missing glyphs in substitution rules (#2654).
+- [mutator/instancer] Added CLI flag to --no-recalc-timestamp (#2649).
+- [SVG] Allow individual SVG documents in SVG OT table to be compressed on uncompressed,
+ and remember that when roundtripping to/from ttx. The SVG.docList is now a list
+ of SVGDocument namedtuple-like dataclass containing an extra ``compressed`` field,
+ and no longer a bare 3-tuple (#2645).
+- [designspaceLib] Check for descriptor types with hasattr() to allow custom classes
+ that don't inherit the default descriptors (#2634).
+- [subset] Enable sharing across subtables of extension lookups for harfbuzz packing
+ (#2626). Updated how table packing falls back to fontTools from harfbuzz (#2668).
+- [subset] Updated default feature tags following current Harfbuzz (#2637).
+- [svgLib] Fixed regex for real number to support e.g. 1e-4 in addition to 1.0e-4.
+ Support parsing negative rx, ry on arc commands (#2596, #2611).
+- [subset] Fixed subsetting SinglePosFormat2 when ValueFormat=0 (#2603).
+
4.33.3 (released 2022-04-26)
----------------------------
diff --git a/Snippets/print-json.py b/Snippets/print-json.py
new file mode 100644
index 00000000..bcd255ee
--- /dev/null
+++ b/Snippets/print-json.py
@@ -0,0 +1,153 @@
+import fontTools.ttLib as ttLib
+from fontTools.ttLib.ttVisitor import TTVisitor
+from fontTools.misc.textTools import Tag
+from array import array
+
+
+class JsonVisitor(TTVisitor):
+ def _open(self, s):
+ print(s, file=self.file)
+ self._indent += self.indent
+ self.comma = False
+
+ def _close(self, s):
+ self._indent = self._indent[: -len(self.indent)]
+ print("\n%s%s" % (self._indent, s), end="", file=self.file)
+ self.comma = True
+
+ def __init__(self, file, indent=" "):
+ self.file = file
+ self.indent = indent
+ self._indent = ""
+
+ def visitObject(self, obj):
+ self._open("{")
+ super().visitObject(obj)
+ if self.comma:
+ print(",", end="", file=self.file)
+ print(
+ '\n%s"type": "%s"' % (self._indent, obj.__class__.__name__),
+ end="",
+ file=self.file,
+ )
+ self._close("}")
+
+ def visitAttr(self, obj, attr, value):
+ if self.comma:
+ print(",", file=self.file)
+ print('%s"%s": ' % (self._indent, attr), end="", file=self.file)
+ self.visit(value)
+ self.comma = True
+
+ def visitList(self, obj, *args, **kwargs):
+ self._open("[")
+ comma = False
+ for value in obj:
+ if comma:
+ print(",", end="", file=self.file)
+ print(file=self.file)
+ print(self._indent, end="", file=self.file)
+ self.visit(value, *args, **kwargs)
+ comma = True
+ self._close("]")
+
+ def visitDict(self, obj, *args, **kwargs):
+ self._open("{")
+ comma = False
+ for key, value in obj.items():
+ if comma:
+ print(",", end="", file=self.file)
+ print(file=self.file)
+ print('%s"%s": ' % (self._indent, key), end="", file=self.file)
+ self.visit(value, *args, **kwargs)
+ comma = True
+ self._close("}")
+
+ def visitLeaf(self, obj):
+ if isinstance(obj, tuple):
+ obj = list(obj)
+ elif isinstance(obj, bytes):
+ obj = list(obj)
+
+ if obj is None:
+ s = "null"
+ elif obj is True:
+ s = "true"
+ elif obj is False:
+ s = "false"
+ else:
+ s = repr(obj)
+
+ if s[0] == "'":
+ s = '"' + s[1:-1] + '"'
+
+ print("%s" % s, end="", file=self.file)
+
+
+@JsonVisitor.register(ttLib.TTFont)
+def visit(self, font):
+ if hasattr(visitor, "font"):
+ print("{}", end="", file=self.file)
+ return False
+ visitor.font = font
+
+ self._open("{")
+ for tag in font.keys():
+ if self.comma:
+ print(",", file=self.file)
+ print('\n%s"%s": ' % (self._indent, tag), end="", file=self.file)
+ visitor.visit(font[tag])
+ self._close("}")
+
+ del visitor.font
+ return False
+
+
+@JsonVisitor.register(ttLib.GlyphOrder)
+def visit(self, obj):
+ self.visitList(self.font.getGlyphOrder())
+ return False
+
+
+@JsonVisitor.register_attr(ttLib.getTableClass("glyf"), "glyphOrder")
+def visit(visitor, obj, attr, value):
+ return False
+
+
+@JsonVisitor.register(ttLib.getTableModule("glyf").GlyphCoordinates)
+def visit(self, obj):
+ self.visitList(obj)
+ return False
+
+
+@JsonVisitor.register(Tag)
+def visit(self, obj):
+ print('"%s"' % str(obj), end="", file=self.file)
+ return False
+
+
+@JsonVisitor.register(array)
+def visit(self, obj):
+ self.visitList(obj)
+ return False
+
+
+@JsonVisitor.register(bytearray)
+def visit(self, obj):
+ self.visitList(obj)
+ return False
+
+
+if __name__ == "__main__":
+
+ from fontTools.ttLib import TTFont
+ import sys
+
+ if len(sys.argv) != 2:
+ print("usage: print-json.py font")
+ sys.exit()
+
+ font = TTFont(sys.argv[1])
+
+ visitor = JsonVisitor(sys.stdout)
+ visitor.visit(font)
diff --git a/Tests/colorLib/builder_test.py b/Tests/colorLib/builder_test.py
index 7259db4d..3cdb2e9a 100644
--- a/Tests/colorLib/builder_test.py
+++ b/Tests/colorLib/builder_test.py
@@ -3,7 +3,7 @@ from fontTools.ttLib import newTable
from fontTools.ttLib.tables import otTables as ot
from fontTools.colorLib import builder
from fontTools.colorLib.geometry import round_start_circle_stable_containment, Circle
-from fontTools.colorLib.builder import LayerListBuilder, _build_n_ary_tree
+from fontTools.colorLib.builder import LayerListBuilder
from fontTools.colorLib.table_builder import TableBuilder
from fontTools.colorLib.errors import ColorLibError
import pytest
@@ -1678,7 +1678,7 @@ class BuildCOLRTest(object):
clipBoxes={
"a": (0, 0, 1000, 1000, 0), # optional 5th: varIndexBase
"c": (-100.8, -200.4, 1100.1, 1200.5), # floats get rounded
- "e": (0, 0, 10, 10), # missing base glyph 'e' is ignored
+ "e": (0, 0, 10, 10), # 'e' does _not_ get ignored despite being missing
},
)
@@ -1689,9 +1689,11 @@ class BuildCOLRTest(object):
] == [
("a", (0, 0, 1000, 1000, 0)),
("c", (-101, -201, 1101, 1201)),
+ ("e", (0, 0, 10, 10)),
]
assert clipBoxes["a"].Format == 2
assert clipBoxes["c"].Format == 1
+ assert clipBoxes["e"].Format == 1
def test_duplicate_base_glyphs(self):
# If > 1 base glyphs refer to equivalent list of layers we expect them to share
@@ -1778,81 +1780,3 @@ class TrickyRadialGradientTest:
)
def test_nudge_start_circle_position(self, c0, r0, c1, r1, inside, expected):
assert self.round_start_circle(c0, r0, c1, r1, inside) == expected
-
-
-@pytest.mark.parametrize(
- "lst, n, expected",
- [
- ([0], 2, [0]),
- ([0, 1], 2, [0, 1]),
- ([0, 1, 2], 2, [[0, 1], 2]),
- ([0, 1, 2], 3, [0, 1, 2]),
- ([0, 1, 2, 3], 2, [[0, 1], [2, 3]]),
- ([0, 1, 2, 3], 3, [[0, 1, 2], 3]),
- ([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]),
- ([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]),
- (list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]),
- (list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]),
- (list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
- (list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]),
- (list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]),
- (list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]),
- (list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]),
- (
- list(range(14)),
- 3,
- [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]],
- ),
- (
- list(range(15)),
- 3,
- [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]],
- ),
- (
- list(range(16)),
- 3,
- [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]],
- ),
- (
- list(range(23)),
- 3,
- [
- [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
- [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
- [[18, 19, 20], 21, 22],
- ],
- ),
- (
- list(range(27)),
- 3,
- [
- [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
- [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
- [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
- ],
- ),
- (
- list(range(28)),
- 3,
- [
- [
- [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
- [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
- [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
- ],
- 27,
- ],
- ),
- (list(range(257)), 256, [list(range(256)), 256]),
- (list(range(258)), 256, [list(range(256)), 256, 257]),
- (list(range(512)), 256, [list(range(256)), list(range(256, 512))]),
- (list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]),
- (
- list(range(256 ** 2)),
- 256,
- [list(range(k * 256, k * 256 + 256)) for k in range(256)],
- ),
- ],
-)
-def test_build_n_ary_tree(lst, n, expected):
- assert _build_n_ary_tree(lst, n) == expected
diff --git a/Tests/colorLib/unbuilder_test.py b/Tests/colorLib/unbuilder_test.py
index 35489680..fe5dc7d5 100644
--- a/Tests/colorLib/unbuilder_test.py
+++ b/Tests/colorLib/unbuilder_test.py
@@ -221,7 +221,26 @@ TEST_COLOR_GLYPHS = {
"Glyph": "glyph00012",
},
],
- }
+ },
+ # When PaintColrLayers contains more than 255 layers, we build a tree
+ # of nested PaintColrLayers of max 255 items (NumLayers field is a uint8).
+ # Below we test that unbuildColrV1 restores a flat list of layers without
+ # nested PaintColrLayers.
+ "glyph00017": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": i,
+ "Alpha": 1.0,
+ },
+ "Glyph": "glyph{str(18 + i).zfill(5)}",
+ }
+ for i in range(256)
+ ],
+ },
}
@@ -230,7 +249,8 @@ def test_unbuildColrV1():
colorGlyphs = unbuildColrV1(layers, baseGlyphs)
assert colorGlyphs == TEST_COLOR_GLYPHS
+
def test_unbuildColrV1_noLayers():
_, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS)
# Just looking to see we don't crash
- unbuildColrV1(None, baseGlyphsV1) \ No newline at end of file
+ unbuildColrV1(None, baseGlyphsV1)
diff --git a/Tests/designspaceLib/data/DS5BreakTest.designspace b/Tests/designspaceLib/data/DS5BreakTest.designspace
new file mode 100644
index 00000000..fec1633c
--- /dev/null
+++ b/Tests/designspaceLib/data/DS5BreakTest.designspace
@@ -0,0 +1,56 @@
+<designspace format="4.0">
+ <axes>
+ <axis default="400" maximum="800" minimum="200" name="Weight" tag="wght">
+ <map input="200" output="0" />
+ <map input="400" output="250" />
+ <map input="800" output="1000" />
+ </axis>
+ </axes>
+
+ <sources>
+ <source familyname="DS5BreakTest" filename="DS5BreakTest-Extralight.ufo" stylename="ExtraLight Condensed">
+ <location>
+ <dimension name="Weight" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="DS5BreakTest" filename="DS5BreakTest-Regular.ufo" stylename="Regular">
+ <location>
+ <dimension name="Weight" xvalue="250" />
+ </location>
+ </source>
+ <source familyname="DS5BreakTest" filename="DS5BreakTest-Extrabold.ufo" stylename="ExtraBold">
+ <location>
+ <dimension name="Weight" xvalue="1000" />
+ </location>
+ </source>
+ </sources>
+
+ <instances>
+ <instance familyname="DS5BreakTest" stylename="ExtraLight">
+ <location>
+ <dimension name="Weight" xvalue="0" />
+ </location>
+ </instance>
+ <instance familyname="DS5BreakTest" stylename="Regular">
+ <location>
+ <dimension name="Weight" xvalue="250" />
+ </location>
+ </instance>
+ <instance familyname="DS5BreakTest" stylename="Medium">
+ <location>
+ <dimension name="Weight" xvalue="400" />
+ </location>
+ </instance>
+ <instance familyname="DS5BreakTest" stylename="Bold">
+ <location>
+ <dimension name="Weight" xvalue="750" />
+ </location>
+ </instance>
+ <instance familyname="DS5BreakTest" stylename="ExtraBold">
+ <location>
+ <dimension name="Weight" xvalue="1000" />
+ </location>
+ </instance>
+ </instances>
+
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5.designspace b/Tests/designspaceLib/data/test_v5.designspace
index 2f611b49..d2b3cdae 100644
--- a/Tests/designspaceLib/data/test_v5.designspace
+++ b/Tests/designspaceLib/data/test_v5.designspace
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
<axes elidedfallbackname="Regular">
- <axis tag="wght" name="weight" minimum="200" maximum="1000" default="200">
+ <axis tag="wght" name="Weight" minimum="200" maximum="1000" default="200">
<labelname xml:lang="en">Wéíght</labelname>
<labelname xml:lang="fa-IR">قطر</labelname>
<map input="200" output="0"/>
@@ -24,7 +24,7 @@
</labels>
</axis>
- <axis tag="wdth" name="width" minimum="50" maximum="150" default="100" hidden="1">
+ <axis tag="wdth" name="Width" minimum="50" maximum="150" default="100" hidden="1">
<labelname xml:lang="fr">Chasse</labelname>
<map input="50" output="10"/>
<map input="100" output="20"/>
@@ -59,15 +59,15 @@
<label name="Some Style">
<labelname xml:lang="fr">Un Style</labelname>
<location>
- <dimension name="weight" uservalue="300"/>
- <dimension name="width" uservalue="50"/>
+ <dimension name="Weight" uservalue="300"/>
+ <dimension name="Width" uservalue="50"/>
<dimension name="Italic" uservalue="0"/>
</location>
</label>
<label name="Other">
<location>
- <dimension name="weight" uservalue="700"/>
- <dimension name="width" uservalue="100"/>
+ <dimension name="Weight" uservalue="700"/>
+ <dimension name="Width" uservalue="100"/>
<dimension name="Italic" uservalue="1"/>
</location>
</label>
@@ -84,7 +84,7 @@
</rules>
<sources>
- <source filename="masters/masterTest1.ufo" name="master.ufo1" familyname="MasterFamilyName" stylename="MasterStyleNameOne">
+ <source filename="masterTest1.ufo" name="master.ufo1" familyname="MasterFamilyName" stylename="MasterStyleNameOne">
<familyname xml:lang="fr">Montserrat</familyname>
<familyname xml:lang="ja">モンセラート</familyname>
<lib copy="1"/>
@@ -93,21 +93,31 @@
<glyph name="A" mute="1"/>
<glyph name="Z" mute="1"/>
<location>
- <dimension name="weight" xvalue="0"/>
- <dimension name="width" xvalue="20"/>
+ <dimension name="Weight" xvalue="0"/>
+ <dimension name="Width" xvalue="20"/>
+ <dimension name="Italic" xvalue="0"/>
</location>
</source>
- <source filename="masters/masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="MasterStyleNameTwo">
+ <source filename="masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="MasterStyleNameTwo">
<kerning mute="1"/>
<location>
- <dimension name="weight" xvalue="1000"/>
- <dimension name="width" xvalue="20"/>
+ <dimension name="Weight" xvalue="1000"/>
+ <dimension name="Width" xvalue="20"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="Supports" layer="supports">
+ <location>
+ <dimension name="Weight" xvalue="1000"/>
+ <dimension name="Width" xvalue="20"/>
+ <dimension name="Italic" xvalue="0"/>
</location>
</source>
- <source filename="masters/masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="Supports" layer="supports">
+ <source filename="masterTest2.ufo" name="master.ufo3" familyname="MasterFamilyName" stylename="FauxItalic">
<location>
- <dimension name="weight" xvalue="1000"/>
- <dimension name="width" xvalue="20"/>
+ <dimension name="Weight" xvalue="0"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
</location>
</source>
</sources>
@@ -193,8 +203,8 @@
<stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
<stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
<location>
- <dimension name="weight" xvalue="500"/>
- <dimension name="width" xvalue="20"/>
+ <dimension name="Weight" xvalue="500"/>
+ <dimension name="Width" xvalue="20"/>
</location>
<!-- The following elements are deprecated in v5.0. They can still be
@@ -220,28 +230,28 @@
</instance>
<instance name="instance.ufo2" familyname="InstanceFamilyName" stylename="InstanceStyleName" filename="instances/instanceTest2.ufo" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName">
<location>
- <dimension name="weight" xvalue="500"/>
- <dimension name="width" xvalue="400" yvalue="300"/>
+ <dimension name="Weight" xvalue="500"/>
+ <dimension name="Width" xvalue="400" yvalue="300"/>
</location>
<!-- ROUNDTRIP_TEST_REMOVE_ME_BEGIN -->
<glyphs>
<glyph unicode="0x65 0xc9 0x12d" name="arrow">
<location>
- <dimension name="weight" xvalue="120"/>
- <dimension name="width" xvalue="100"/>
+ <dimension name="Weight" xvalue="120"/>
+ <dimension name="Width" xvalue="100"/>
</location>
<note>A note about this glyph</note>
<masters>
<master glyphname="BB" source="master.ufo1">
<location>
- <dimension name="weight" xvalue="20"/>
- <dimension name="width" xvalue="20"/>
+ <dimension name="Weight" xvalue="20"/>
+ <dimension name="Width" xvalue="20"/>
</location>
</master>
<master glyphname="CC" source="master.ufo2">
<location>
- <dimension name="weight" xvalue="900"/>
- <dimension name="width" xvalue="900"/>
+ <dimension name="Weight" xvalue="900"/>
+ <dimension name="Width" xvalue="900"/>
</location>
</master>
</masters>
@@ -262,24 +272,24 @@
- with user coordinates (uservalue="")
- with a mix of both coordinate systems
-->
- <instance location="asdf"/>
+ <instance location="Some Style"/>
<instance>
<location>
- <dimension name="weight" xvalue="600"/>
- <dimension name="width" xvalue="401" yvalue="420"/>
+ <dimension name="Weight" xvalue="600"/>
+ <dimension name="Width" xvalue="401" yvalue="420"/>
</location>
</instance>
<instance>
<location>
- <dimension name="weight" xvalue="10"/>
- <dimension name="width" uservalue="100"/>
+ <dimension name="Weight" xvalue="10"/>
+ <dimension name="Width" uservalue="100"/>
<dimension name="Italic" xvalue="0"/>
</location>
</instance>
<instance>
<location>
- <dimension name="weight" uservalue="300"/>
- <dimension name="width" uservalue="130"/>
+ <dimension name="Weight" uservalue="300"/>
+ <dimension name="Width" uservalue="130"/>
<dimension name="Italic" uservalue="1"/>
</location>
</instance>
diff --git a/Tests/designspaceLib/designspace_v5_test.py b/Tests/designspaceLib/designspace_v5_test.py
index 9e803340..35ad29b2 100644
--- a/Tests/designspaceLib/designspace_v5_test.py
+++ b/Tests/designspaceLib/designspace_v5_test.py
@@ -34,7 +34,7 @@ def test_read_v5_document_simple(datadir):
[
AxisDescriptor(
tag="wght",
- name="weight",
+ name="Weight",
minimum=200,
maximum=1000,
default=200,
@@ -82,7 +82,7 @@ def test_read_v5_document_simple(datadir):
),
AxisDescriptor(
tag="wdth",
- name="width",
+ name="Width",
minimum=50,
maximum=150,
default=100,
@@ -123,10 +123,10 @@ def test_read_v5_document_simple(datadir):
LocationLabelDescriptor(
name="Some Style",
labelNames={"fr": "Un Style"},
- userLocation={"weight": 300, "width": 50, "Italic": 0},
+ userLocation={"Weight": 300, "Width": 50, "Italic": 0},
),
LocationLabelDescriptor(
- name="Other", userLocation={"weight": 700, "width": 100, "Italic": 1}
+ name="Other", userLocation={"Weight": 700, "Width": 100, "Italic": 1}
),
],
)
@@ -139,7 +139,7 @@ def test_read_v5_document_simple(datadir):
path=posix(str((datadir / "masters/masterTest1.ufo").resolve())),
name="master.ufo1",
layerName=None,
- location={"weight": 0.0, "width": 20.0},
+ location={"Italic": 0.0, "Weight": 0.0, "Width": 20.0},
copyLib=True,
copyInfo=True,
copyGroups=False,
@@ -156,7 +156,7 @@ def test_read_v5_document_simple(datadir):
path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
name="master.ufo2",
layerName=None,
- location={"weight": 1000.0, "width": 20.0},
+ location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0},
copyLib=False,
copyInfo=False,
copyGroups=False,
@@ -173,7 +173,7 @@ def test_read_v5_document_simple(datadir):
path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
name="master.ufo2",
layerName="supports",
- location={"weight": 1000.0, "width": 20.0},
+ location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0},
copyLib=False,
copyInfo=False,
copyGroups=False,
@@ -185,6 +185,22 @@ def test_read_v5_document_simple(datadir):
styleName="Supports",
localisedFamilyName={},
),
+ SourceDescriptor(
+ filename="masters/masterTest2.ufo",
+ path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
+ name="master.ufo3",
+ layerName=None,
+ location={"Italic": 1.0, "Weight": 0.0, "Width": 100.0},
+ copyLib=False,
+ copyGroups=False,
+ copyFeatures=False,
+ muteKerning=False,
+ muteInfo=False,
+ mutedGlyphNames=[],
+ familyName="MasterFamilyName",
+ styleName="FauxItalic",
+ localisedFamilyName={},
+ ),
],
)
@@ -245,7 +261,7 @@ def test_read_v5_document_simple(datadir):
filename="instances/instanceTest1.ufo",
path=posix(str((datadir / "instances/instanceTest1.ufo").resolve())),
name="instance.ufo1",
- designLocation={"weight": 500.0, "width": 20.0},
+ designLocation={"Weight": 500.0, "Width": 20.0},
familyName="InstanceFamilyName",
styleName="InstanceStyleName",
postScriptFontName="InstancePostscriptName",
@@ -268,7 +284,7 @@ def test_read_v5_document_simple(datadir):
filename="instances/instanceTest2.ufo",
path=posix(str((datadir / "instances/instanceTest2.ufo").resolve())),
name="instance.ufo2",
- designLocation={"weight": 500.0, "width": (400.0, 300.0)},
+ designLocation={"Weight": 500.0, "Width": (400.0, 300.0)},
familyName="InstanceFamilyName",
styleName="InstanceStyleName",
postScriptFontName="InstancePostscriptName",
@@ -278,16 +294,16 @@ def test_read_v5_document_simple(datadir):
"arrow": {
"unicodes": [101, 201, 301],
"note": "A note about this glyph",
- "instanceLocation": {"weight": 120.0, "width": 100.0},
+ "instanceLocation": {"Weight": 120.0, "Width": 100.0},
"masters": [
{
"font": "master.ufo1",
- "location": {"weight": 20.0, "width": 20.0},
+ "location": {"Weight": 20.0, "Width": 20.0},
"glyphName": "BB",
},
{
"font": "master.ufo2",
- "location": {"weight": 900.0, "width": 900.0},
+ "location": {"Weight": 900.0, "Width": 900.0},
"glyphName": "CC",
},
],
@@ -296,17 +312,17 @@ def test_read_v5_document_simple(datadir):
},
),
InstanceDescriptor(
- locationLabel="asdf",
+ locationLabel="Some Style",
),
InstanceDescriptor(
- designLocation={"weight": 600.0, "width": (401.0, 420.0)},
+ designLocation={"Weight": 600.0, "Width": (401.0, 420.0)},
),
InstanceDescriptor(
- designLocation={"weight": 10.0, "Italic": 0.0},
- userLocation={"width": 100.0},
+ designLocation={"Weight": 10.0, "Italic": 0.0},
+ userLocation={"Width": 100.0},
),
InstanceDescriptor(
- userLocation={"weight": 300.0, "width": 130.0, "Italic": 1.0},
+ userLocation={"Weight": 300.0, "Width": 130.0, "Italic": 1.0},
),
],
)
diff --git a/Tests/designspaceLib/statNames_test.py b/Tests/designspaceLib/statNames_test.py
index 076abc90..99d1c7fa 100644
--- a/Tests/designspaceLib/statNames_test.py
+++ b/Tests/designspaceLib/statNames_test.py
@@ -19,7 +19,7 @@ def test_instance_getStatNames(datadir):
def test_not_all_ordering_specified_and_translations(datadir):
doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
- assert getStatNames(doc, {"weight": 200, "width": 125, "Italic": 1}) == StatNames(
+ assert getStatNames(doc, {"Weight": 200, "Width": 125, "Italic": 1}) == StatNames(
familyNames={
"en": "MasterFamilyName",
"fr": "Montserrat",
@@ -59,3 +59,20 @@ def test_detect_ribbi_aktiv(datadir):
styleMapFamilyNames={"en": "Aktiv Grotesk Cd"},
styleMapStyleName="bold italic",
)
+
+
+def test_getStatNames_on_ds4_doesnt_make_up_bad_names(datadir):
+ """See this issue on GitHub: https://github.com/googlefonts/ufo2ft/issues/630
+
+ When as in the example, there's no STAT data present, the getStatName
+ shouldn't try making up a postscript name.
+ """
+ doc = DesignSpaceDocument.fromfile(datadir / "DS5BreakTest.designspace")
+
+ assert getStatNames(doc, {"Weight": 600, "Width": 125, "Italic": 1}) == StatNames(
+ familyNames={"en": "DS5BreakTest"},
+ styleNames={},
+ postScriptFontName=None,
+ styleMapFamilyNames={},
+ styleMapStyleName=None,
+ )
diff --git a/Tests/feaLib/data/name.fea b/Tests/feaLib/data/name.fea
index 17727ed9..7c94b87f 100644
--- a/Tests/feaLib/data/name.fea
+++ b/Tests/feaLib/data/name.fea
@@ -1,16 +1,16 @@
table name {
#test-fea2fea:
- nameid 1 "Ignored-1";
+ nameid 1 "Test1";
#test-fea2fea:
- nameid 2 "Ignored-2";
+ nameid 2 "Test2";
#test-fea2fea:
- nameid 3 "Ignored-3";
+ nameid 3 "Test3";
#test-fea2fea:
- nameid 4 "Ignored-4";
+ nameid 4 "Test4";
#test-fea2fea:
- nameid 5 "Ignored-5";
+ nameid 5 "Test5";
#test-fea2fea:
- nameid 6 "Ignored-6";
+ nameid 6 "Test6";
#test-fea2fea: nameid 7 "Test7";
nameid 7 3 "Test7";
nameid 8 1 "Test8";
diff --git a/Tests/feaLib/data/name.ttx b/Tests/feaLib/data/name.ttx
index cdb60382..5014b251 100644
--- a/Tests/feaLib/data/name.ttx
+++ b/Tests/feaLib/data/name.ttx
@@ -2,6 +2,24 @@
<ttFont>
<name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Test1
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Test2
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ Test3
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Test4
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Test5
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ Test6
+ </namerecord>
<namerecord nameID="7" platformID="3" platEncID="1" langID="0x409">
Test7
</namerecord>
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index fd9dea70..b281e8ac 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -316,7 +316,7 @@ class ParserTest(unittest.TestCase):
def test_strict_glyph_name_check(self):
self.parse("@bad = [a b ccc];", glyphNames=("a", "b", "ccc"))
- with self.assertRaisesRegex(FeatureLibError, "missing from the glyph set: ccc"):
+ with self.assertRaisesRegex(FeatureLibError, "(?s)missing from the glyph set:.*ccc"):
self.parse("@bad = [a b ccc];", glyphNames=("a", "b"))
def test_glyphclass(self):
diff --git a/Tests/merge/data/CFFFont_expected.ttx b/Tests/merge/data/CFFFont_expected.ttx
index c8870e44..2c4cd33e 100644
--- a/Tests/merge/data/CFFFont_expected.ttx
+++ b/Tests/merge/data/CFFFont_expected.ttx
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
-<ttFont sfntVersion="OTTO" ttLibVersion="4.28">
+<ttFont sfntVersion="OTTO" ttLibVersion="4.34">
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
@@ -507,7 +507,7 @@
<GlyphID id="501" name="vabove-ar"/>
<GlyphID id="502" name="vbelow-ar"/>
<GlyphID id="503" name="opendammatan-ar"/>
- <GlyphID id="504" name=".notdef#1"/>
+ <GlyphID id="504" name=".notdef.1"/>
<GlyphID id="505" name="A"/>
<GlyphID id="506" name="Aacute"/>
<GlyphID id="507" name="Acircumflex"/>
@@ -701,12 +701,12 @@
<GlyphID id="695" name="onehalf"/>
<GlyphID id="696" name="onequarter"/>
<GlyphID id="697" name="threequarters"/>
- <GlyphID id="698" name="space#1"/>
+ <GlyphID id="698" name="space.1"/>
<GlyphID id="699" name="period"/>
<GlyphID id="700" name="comma"/>
<GlyphID id="701" name="colon"/>
<GlyphID id="702" name="semicolon"/>
- <GlyphID id="703" name="exclam#1"/>
+ <GlyphID id="703" name="exclam.1"/>
<GlyphID id="704" name="exclamdown"/>
<GlyphID id="705" name="question"/>
<GlyphID id="706" name="questiondown"/>
@@ -725,8 +725,8 @@
<GlyphID id="719" name="braceright"/>
<GlyphID id="720" name="bracketleft"/>
<GlyphID id="721" name="bracketright"/>
- <GlyphID id="722" name="quoteleft#1"/>
- <GlyphID id="723" name="quoteright#1"/>
+ <GlyphID id="722" name="quoteleft.1"/>
+ <GlyphID id="723" name="quoteright.1"/>
<GlyphID id="724" name="guillemotleft"/>
<GlyphID id="725" name="guillemotright"/>
<GlyphID id="726" name="guilsinglleft"/>
@@ -788,12 +788,12 @@
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="1.003"/>
- <checkSumAdjustment value="0x490fa241"/>
+ <checkSumAdjustment value="0x9a87f91"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00000011"/>
<unitsPerEm value="1000"/>
- <created value="Thu Jan 1 00:00:00 1970"/>
- <modified value="Thu Jan 1 00:00:00 1970"/>
+ <created value="Sun Aug 14 18:30:31 2022"/>
+ <modified value="Sun Aug 14 18:30:31 2022"/>
<xMin value="-199"/>
<yMin value="-364"/>
<xMax value="1459"/>
@@ -1380,7 +1380,7 @@
900 300 -900 -300 vlineto
endchar
</CharString>
- <CharString name=".notdef#1">
+ <CharString name=".notdef.1">
-45 50 -200 rmoveto
400 1000 -400 -1000 hlineto
50 50 rmoveto
@@ -4962,7 +4962,7 @@
-4 3 -5 1 -3 -4 rrcurveto
endchar
</CharString>
- <CharString name="exclam#1">
+ <CharString name="exclam.1">
-329 112 186 rmoveto
36 371 rlineto
3 29 2 29 29 vvcurveto
@@ -9687,7 +9687,7 @@
-21 14 15 -11 14 hhcurveto
endchar
</CharString>
- <CharString name="quoteleft#1">
+ <CharString name="quoteleft.1">
-345 115 532 rmoveto
-9 14 -5 15 16 vvcurveto
35 28 47 21 36 vhcurveto
@@ -9709,7 +9709,7 @@
21 -14 -15 11 -14 hhcurveto
endchar
</CharString>
- <CharString name="quoteright#1">
+ <CharString name="quoteright.1">
-348 66 395 rmoveto
35 53 54 54 62 vvcurveto
42 -43 89 -28 -16 -32 -26 -15 -7 8 -16 6 -10 vhcurveto
@@ -11038,7 +11038,7 @@
<CharString name="space">
-218 endchar
</CharString>
- <CharString name="space#1">
+ <CharString name="space.1">
-212 endchar
</CharString>
<CharString name="sterling">
@@ -29261,7 +29261,7 @@
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
- <Substitution in="space#1" out="space#1"/>
+ <Substitution in="space.1" out="space.1"/>
</SingleSubst>
</Lookup>
<Lookup index="96">
@@ -29269,10 +29269,10 @@
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
- <Substitution in="exclam" out="exclam#1"/>
- <Substitution in="quoteleft" out="quoteleft#1"/>
- <Substitution in="quoteright" out="quoteright#1"/>
- <Substitution in="space" out="space#1"/>
+ <Substitution in="exclam" out="exclam.1"/>
+ <Substitution in="quoteleft" out="quoteleft.1"/>
+ <Substitution in="quoteright" out="quoteright.1"/>
+ <Substitution in="space" out="space.1"/>
</SingleSubst>
</Lookup>
</LookupList>
@@ -29280,7 +29280,7 @@
<hmtx>
<mtx name=".notdef" width="500" lsb="50"/>
- <mtx name=".notdef#1" width="500" lsb="50"/>
+ <mtx name=".notdef.1" width="500" lsb="50"/>
<mtx name="A" width="722" lsb="3"/>
<mtx name="AE" width="797" lsb="3"/>
<mtx name="Aacute" width="722" lsb="3"/>
@@ -29503,7 +29503,7 @@
<mtx name="eth" width="461" lsb="28"/>
<mtx name="euro" width="638" lsb="25"/>
<mtx name="exclam" width="253" lsb="26"/>
- <mtx name="exclam#1" width="216" lsb="54"/>
+ <mtx name="exclam.1" width="216" lsb="54"/>
<mtx name="exclamdown" width="216" lsb="54"/>
<mtx name="f" width="329" lsb="-1"/>
<mtx name="f.alt" width="317" lsb="-1"/>
@@ -29848,9 +29848,9 @@
<mtx name="quotedblleft" width="444" lsb="91"/>
<mtx name="quotedblright" width="444" lsb="58"/>
<mtx name="quoteleft" width="271" lsb="91"/>
- <mtx name="quoteleft#1" width="200" lsb="41"/>
+ <mtx name="quoteleft.1" width="200" lsb="41"/>
<mtx name="quoteright" width="271" lsb="58"/>
- <mtx name="quoteright#1" width="197" lsb="36"/>
+ <mtx name="quoteright.1" width="197" lsb="36"/>
<mtx name="quotesinglbase" width="271" lsb="58"/>
<mtx name="quotesingle" width="210" lsb="74"/>
<mtx name="r" width="416" lsb="0"/>
@@ -29948,7 +29948,7 @@
<mtx name="slash" width="390" lsb="-17"/>
<mtx name="softhyphen" width="0" lsb="0"/>
<mtx name="space" width="146" lsb="0"/>
- <mtx name="space#1" width="333" lsb="0"/>
+ <mtx name="space.1" width="333" lsb="0"/>
<mtx name="sterling" width="649" lsb="22"/>
<mtx name="sukun-ar" width="0" lsb="-98"/>
<mtx name="sukun-ar.alt" width="0" lsb="-53"/>
diff --git a/Tests/misc/treeTools_test.py b/Tests/misc/treeTools_test.py
new file mode 100644
index 00000000..467a5c57
--- /dev/null
+++ b/Tests/misc/treeTools_test.py
@@ -0,0 +1,80 @@
+from fontTools.misc.treeTools import build_n_ary_tree
+import pytest
+
+
+@pytest.mark.parametrize(
+ "lst, n, expected",
+ [
+ ([0], 2, [0]),
+ ([0, 1], 2, [0, 1]),
+ ([0, 1, 2], 2, [[0, 1], 2]),
+ ([0, 1, 2], 3, [0, 1, 2]),
+ ([0, 1, 2, 3], 2, [[0, 1], [2, 3]]),
+ ([0, 1, 2, 3], 3, [[0, 1, 2], 3]),
+ ([0, 1, 2, 3, 4], 3, [[0, 1, 2], 3, 4]),
+ ([0, 1, 2, 3, 4, 5], 3, [[0, 1, 2], [3, 4, 5]]),
+ (list(range(7)), 3, [[0, 1, 2], [3, 4, 5], 6]),
+ (list(range(8)), 3, [[0, 1, 2], [3, 4, 5], [6, 7]]),
+ (list(range(9)), 3, [[0, 1, 2], [3, 4, 5], [6, 7, 8]]),
+ (list(range(10)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9]),
+ (list(range(11)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], 9, 10]),
+ (list(range(12)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11]]),
+ (list(range(13)), 3, [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], 12]),
+ (
+ list(range(14)),
+ 3,
+ [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], 12, 13]],
+ ),
+ (
+ list(range(15)),
+ 3,
+ [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [9, 10, 11], [12, 13, 14]],
+ ),
+ (
+ list(range(16)),
+ 3,
+ [[[0, 1, 2], [3, 4, 5], [6, 7, 8]], [[9, 10, 11], [12, 13, 14], 15]],
+ ),
+ (
+ list(range(23)),
+ 3,
+ [
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+ [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+ [[18, 19, 20], 21, 22],
+ ],
+ ),
+ (
+ list(range(27)),
+ 3,
+ [
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+ [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+ [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
+ ],
+ ),
+ (
+ list(range(28)),
+ 3,
+ [
+ [
+ [[0, 1, 2], [3, 4, 5], [6, 7, 8]],
+ [[9, 10, 11], [12, 13, 14], [15, 16, 17]],
+ [[18, 19, 20], [21, 22, 23], [24, 25, 26]],
+ ],
+ 27,
+ ],
+ ),
+ (list(range(257)), 256, [list(range(256)), 256]),
+ (list(range(258)), 256, [list(range(256)), 256, 257]),
+ (list(range(512)), 256, [list(range(256)), list(range(256, 512))]),
+ (list(range(512 + 1)), 256, [list(range(256)), list(range(256, 512)), 512]),
+ (
+ list(range(256 ** 2)),
+ 256,
+ [list(range(k * 256, k * 256 + 256)) for k in range(256)],
+ ),
+ ],
+)
+def test_build_n_ary_tree(lst, n, expected):
+ assert build_n_ary_tree(lst, n) == expected
diff --git a/Tests/misc/visitor_test.py b/Tests/misc/visitor_test.py
new file mode 100644
index 00000000..fe71e08f
--- /dev/null
+++ b/Tests/misc/visitor_test.py
@@ -0,0 +1,72 @@
+from fontTools.misc.visitor import Visitor
+import enum
+import pytest
+
+
+class E(enum.Enum):
+ E1 = 1
+ E2 = 2
+ E3 = 3
+
+class A:
+ def __init__(self):
+ self.a = 1
+ self.b = [2, 3]
+ self.c = {4: 5, 6: 7}
+ self._d = 8
+ self.e = E.E2
+ self.f = 10
+
+
+class B:
+ def __init__(self):
+ self.a = A()
+
+
+class TestVisitor(Visitor):
+ def __init__(self):
+ self.value = []
+
+ def _add(self, s):
+ self.value.append(s)
+
+ def visitLeaf(self, obj):
+ self._add(obj)
+ super().visitLeaf(obj)
+
+
+@TestVisitor.register(A)
+def visit(self, obj):
+ self._add("A")
+
+
+@TestVisitor.register_attrs([(A, "e")])
+def visit(self, obj, attr, value):
+ self._add(attr)
+ self._add(value)
+ return False
+
+
+@TestVisitor.register(B)
+def visit(self, obj):
+ self._add("B")
+ self.visitObject(obj)
+ return False
+
+
+@TestVisitor.register_attr(B, "a")
+def visit(self, obj, attr, value):
+ self._add("B a")
+
+
+class VisitorTest(object):
+ def test_visitor(self):
+ b = B()
+ visitor = TestVisitor()
+ visitor.visit(b)
+ assert visitor.value == ["B", "B a", "A", 1, 2, 3, 5, 7, "e", E.E2, 10]
+
+ visitor.value = []
+ visitor.defaultStop = True
+ visitor.visit(b)
+ assert visitor.value == ["B", "B a"]
diff --git a/Tests/pens/cu2quPen_test.py b/Tests/pens/cu2quPen_test.py
index db517879..4ce5b512 100644
--- a/Tests/pens/cu2quPen_test.py
+++ b/Tests/pens/cu2quPen_test.py
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import sys
import unittest
from fontTools.pens.cu2quPen import Cu2QuPen, Cu2QuPointPen
@@ -257,8 +258,12 @@ class TestCu2QuPen(unittest.TestCase, _TestPenMixin):
quadpen.closePath()
self.assertGreaterEqual(len(log.records), 1)
- self.assertIn("ignore_single_points is deprecated",
- log.records[0].args[0])
+ if sys.version_info < (3, 11):
+ self.assertIn("ignore_single_points is deprecated",
+ log.records[0].args[0])
+ else:
+ self.assertIn("ignore_single_points is deprecated",
+ log.records[0].msg)
# single-point contours were ignored, so the pen commands are empty
self.assertFalse(pen.commands)
diff --git a/Tests/subset/data/expect_no_notdef_outline_cid.ttx b/Tests/subset/data/expect_no_notdef_outline_cid.ttx
index 5167c2cf..990caa5c 100644
--- a/Tests/subset/data/expect_no_notdef_outline_cid.ttx
+++ b/Tests/subset/data/expect_no_notdef_outline_cid.ttx
@@ -23,7 +23,7 @@
<CIDFontType value="0"/>
<CIDCount value="4"/>
<!-- charset is dumped separately as the 'GlyphOrder' element -->
- <FDSelect format="3"/>
+ <FDSelect format="0"/>
<FDArray>
<FontDict index="0">
<FontName value="TestCID-Regular-One"/>
diff --git a/Tests/subset/data/expect_notdef_width_cid.ttx b/Tests/subset/data/expect_notdef_width_cid.ttx
index ccd0f65f..8ea77e5e 100644
--- a/Tests/subset/data/expect_notdef_width_cid.ttx
+++ b/Tests/subset/data/expect_notdef_width_cid.ttx
@@ -23,7 +23,7 @@
<CIDFontType value="0"/>
<CIDCount value="4"/>
<!-- charset is dumped separately as the 'GlyphOrder' element -->
- <FDSelect format="3"/>
+ <FDSelect format="0"/>
<FDArray>
<FontDict index="0">
<FontName value="NotdefWidthCID-Regular-Space"/>
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index facafb2a..7efcb698 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -819,13 +819,20 @@ class SubsetTest:
if not have_uharfbuzz:
pytest.skip("uharfbuzz is not installed")
if not ok:
- # pretend hb.repack returns an error
+ # pretend hb.repack/repack_with_tag return an error
import uharfbuzz as hb
def mock_repack(data, obj_list):
raise hb.RepackerError("mocking")
monkeypatch.setattr(hb, "repack", mock_repack)
+
+ if hasattr(hb, "repack_with_tag"): # uharfbuzz >= 0.30.0
+
+ def mock_repack_with_tag(tag, data, obj_list):
+ raise hb.RepackerError("mocking")
+
+ monkeypatch.setattr(hb, "repack_with_tag", mock_repack_with_tag)
else:
if have_uharfbuzz:
# pretend uharfbuzz is not installed
@@ -879,8 +886,8 @@ class SubsetTest:
# test we emit a log.error if hb.repack fails (and we don't if successful)
assert (
(
- "hb.repack failed to serialize 'GSUB', reverting to "
- "pure-python serializer; the error message was: RepackerError: mocking"
+ "hb.repack failed to serialize 'GSUB', attempting fonttools resolutions "
+ "; the error message was: RepackerError: mocking"
) in caplog.text
) ^ ok
@@ -953,7 +960,8 @@ def test_subset_feature_variations_drop_all(featureVarsTestFont):
# https://github.com/fonttools/fonttools/issues/1881#issuecomment-619415044
-def test_subset_single_pos_format():
+@pytest.fixture
+def singlepos2_font():
fb = FontBuilder(unitsPerEm=1000)
fb.setupGlyphOrder([".notdef", "a", "b", "c"])
fb.setupCharacterMap({ord("a"): "a", ord("b"): "b", ord("c"): "c"})
@@ -971,8 +979,11 @@ def test_subset_single_pos_format():
fb.save(buf)
buf.seek(0)
- font = TTFont(buf)
+ return TTFont(buf)
+
+def test_subset_single_pos_format(singlepos2_font):
+ font = singlepos2_font
# The input font has a SinglePos Format 2 subtable where each glyph has
# different ValueRecords
assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [
@@ -1018,6 +1029,46 @@ def test_subset_single_pos_format():
'</Lookup>',
]
+def test_subset_single_pos_format2_all_None(singlepos2_font):
+ # https://github.com/fonttools/fonttools/issues/2602
+ font = singlepos2_font
+ gpos = font["GPOS"].table
+ subtable = gpos.LookupList.Lookup[0].SubTable[0]
+ assert subtable.Format == 2
+ # Hack a SinglePosFormat2 with ValueFormat = 0; our own buildSinglePos
+ # never makes these as a SinglePosFormat1 is more compact, but they can
+ # be found in the wild.
+ subtable.Value = [None] * subtable.ValueCount
+ subtable.ValueFormat = 0
+
+ assert getXML(subtable.toXML, font) == [
+ '<SinglePos Format="2">',
+ ' <Coverage>',
+ ' <Glyph value="a"/>',
+ ' <Glyph value="b"/>',
+ ' <Glyph value="c"/>',
+ ' </Coverage>',
+ ' <ValueFormat value="0"/>',
+ ' <!-- ValueCount=3 -->',
+ '</SinglePos>',
+ ]
+
+ options = subset.Options()
+ subsetter = subset.Subsetter(options)
+ subsetter.populate(unicodes=[ord("a"), ord("c")])
+ subsetter.subset(font)
+
+ # Check it was downgraded to Format1 after subsetting
+ assert getXML(font["GPOS"].table.LookupList.Lookup[0].SubTable[0].toXML, font) == [
+ '<SinglePos Format="1">',
+ ' <Coverage>',
+ ' <Glyph value="a"/>',
+ ' <Glyph value="c"/>',
+ ' </Coverage>',
+ ' <ValueFormat value="0"/>',
+ '</SinglePos>',
+ ]
+
@pytest.fixture
def ttf_path(tmp_path):
@@ -1426,7 +1477,7 @@ def test_subset_svg_missing_lxml(ttf_path):
font["SVG "].docList = [('<svg><g id="glyph1"/></svg>', 1, 1)]
font.save(ttf_path)
- with pytest.raises(ModuleNotFoundError):
+ with pytest.raises(ImportError):
subset.main([str(ttf_path), "--gids=0,1"])
diff --git a/Tests/svgLib/path/parser_test.py b/Tests/svgLib/path/parser_test.py
index b533dd8e..d33043fc 100644
--- a/Tests/svgLib/path/parser_test.py
+++ b/Tests/svgLib/path/parser_test.py
@@ -280,6 +280,15 @@ def test_exponents():
assert pen.value == expected
+ pen = RecordingPen()
+ parse_path("M-3e38 3E+38L-3E-38,3e-38", pen)
+ expected = [
+ ("moveTo", ((-3e+38, 3e+38),)),
+ ("lineTo", ((-3e-38, 3e-38),)),
+ ("endPath", ()),
+ ]
+
+ assert pen.value == expected
def test_invalid_implicit_command():
with pytest.raises(ValueError) as exc_info:
@@ -360,7 +369,7 @@ def test_arc_pen_with_arcTo():
"M1-2A3-4-1.0 01.5.7",
[
("moveTo", ((1.0, -2.0),)),
- ("arcTo", (3.0, -4.0, -1.0, False, True, (0.5, 0.7))),
+ ("arcTo", (3.0, 4.0, -1.0, False, True, (0.5, 0.7))),
("endPath", ()),
],
),
diff --git a/Tests/ttLib/data/I-512upem.ttx b/Tests/ttLib/data/I-512upem.ttx
new file mode 100644
index 00000000..34795b1f
--- /dev/null
+++ b/Tests/ttLib/data/I-512upem.ttx
@@ -0,0 +1,3777 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.36">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="I"/>
+ </GlyphOrder>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="475"/>
+ <descent value="-125"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="227"/>
+ <minLeftSideBearing value="44"/>
+ <minRightSideBearing value="44"/>
+ <xMaxExtent value="92"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="2"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="2"/>
+ <maxPoints value="4"/>
+ <maxContours value="1"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="286"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000000"/>
+ <ySubscriptXSize value="359"/>
+ <ySubscriptYSize value="333"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="72"/>
+ <ySuperscriptXSize value="359"/>
+ <ySuperscriptYSize value="333"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="244"/>
+ <yStrikeoutSize value="26"/>
+ <yStrikeoutPosition value="128"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="2"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="GOOG"/>
+ <fsSelection value="00000000 11000000"/>
+ <usFirstCharIndex value="73"/>
+ <usLastCharIndex value="73"/>
+ <sTypoAscender value="475"/>
+ <sTypoDescender value="-125"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="615"/>
+ <usWinDescent value="150"/>
+ <ulCodePageRange1 value="00100000 00000000 00000001 10011111"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="263"/>
+ <sCapHeight value="364"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="4"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="227" lsb="25"/>
+ <mtx name="I" width="136" lsb="44"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x49" name="I"/><!-- LATIN CAPITAL LETTER I -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x49" name="I"/><!-- LATIN CAPITAL LETTER I -->
+ </cmap_format_4>
+ </cmap>
+
+ <prep>
+ <assembly>
+ PUSHW[ ] /* 1 value pushed */
+ 511
+ SCANCTRL[ ] /* ScanConversionControl */
+ PUSHB[ ] /* 1 value pushed */
+ 4
+ SCANTYPE[ ] /* ScanType */
+ </assembly>
+ </prep>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef"/><!-- contains no outline data -->
+
+ <TTGlyph name="I" xMin="44" yMin="0" xMax="92" yMax="364">
+ <contour>
+ <pt x="44" y="0" on="1" overlap="1"/>
+ <pt x="92" y="0" on="1"/>
+ <pt x="92" y="364" on="1"/>
+ <pt x="44" y="364" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright 2017 The Roboto Flex Project Authors (https://github.com/TypeNetwork/Roboto-Flex)
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Roboto Flex
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ Google:Roboto Regular:2017
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Roboto Flex Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 3.100
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlex-Regular
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ wght
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
+ wdth
+ </namerecord>
+ <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
+ opsz
+ </namerecord>
+ <namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
+ GRAD
+ </namerecord>
+ <namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
+ slnt
+ </namerecord>
+ <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
+ XTRA
+ </namerecord>
+ <namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
+ XOPQ
+ </namerecord>
+ <namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
+ YOPQ
+ </namerecord>
+ <namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
+ YTLC
+ </namerecord>
+ <namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
+ YTUC
+ </namerecord>
+ <namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
+ YTAS
+ </namerecord>
+ <namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
+ YTDE
+ </namerecord>
+ <namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
+ YTFI
+ </namerecord>
+ <namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
+ Thin
+ </namerecord>
+ <namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
+ ExtraLight
+ </namerecord>
+ <namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
+ Light
+ </namerecord>
+ <namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
+ Medium
+ </namerecord>
+ <namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
+ SemiBold
+ </namerecord>
+ <namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
+ Bold
+ </namerecord>
+ <namerecord nameID="276" platformID="3" platEncID="1" langID="0x409">
+ ExtraBold
+ </namerecord>
+ <namerecord nameID="277" platformID="3" platEncID="1" langID="0x409">
+ Black
+ </namerecord>
+ <namerecord nameID="278" platformID="3" platEncID="1" langID="0x409">
+ ExtraBlack
+ </namerecord>
+ <namerecord nameID="279" platformID="3" platEncID="1" langID="0x409">
+ Thin Italic
+ </namerecord>
+ <namerecord nameID="280" platformID="3" platEncID="1" langID="0x409">
+ ExtraLight Italic
+ </namerecord>
+ <namerecord nameID="281" platformID="3" platEncID="1" langID="0x409">
+ Light Italic
+ </namerecord>
+ <namerecord nameID="282" platformID="3" platEncID="1" langID="0x409">
+ Italic
+ </namerecord>
+ <namerecord nameID="283" platformID="3" platEncID="1" langID="0x409">
+ Medium Italic
+ </namerecord>
+ <namerecord nameID="284" platformID="3" platEncID="1" langID="0x409">
+ SemiBold Italic
+ </namerecord>
+ <namerecord nameID="285" platformID="3" platEncID="1" langID="0x409">
+ Bold Italic
+ </namerecord>
+ <namerecord nameID="286" platformID="3" platEncID="1" langID="0x409">
+ ExtraBold Italic
+ </namerecord>
+ <namerecord nameID="287" platformID="3" platEncID="1" langID="0x409">
+ Black Italic
+ </namerecord>
+ <namerecord nameID="288" platformID="3" platEncID="1" langID="0x409">
+ ExtraBlack Italic
+ </namerecord>
+ <namerecord nameID="289" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Thin
+ </namerecord>
+ <namerecord nameID="290" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLight
+ </namerecord>
+ <namerecord nameID="291" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Light
+ </namerecord>
+ <namerecord nameID="292" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Regular
+ </namerecord>
+ <namerecord nameID="293" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Medium
+ </namerecord>
+ <namerecord nameID="294" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBold
+ </namerecord>
+ <namerecord nameID="295" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Bold
+ </namerecord>
+ <namerecord nameID="296" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBold
+ </namerecord>
+ <namerecord nameID="297" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Black
+ </namerecord>
+ <namerecord nameID="298" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlack
+ </namerecord>
+ <namerecord nameID="299" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ThinItalic
+ </namerecord>
+ <namerecord nameID="300" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLightItalic
+ </namerecord>
+ <namerecord nameID="301" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-LightItalic
+ </namerecord>
+ <namerecord nameID="302" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Italic
+ </namerecord>
+ <namerecord nameID="303" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-MediumItalic
+ </namerecord>
+ <namerecord nameID="304" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBoldItalic
+ </namerecord>
+ <namerecord nameID="305" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BoldItalic
+ </namerecord>
+ <namerecord nameID="306" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBoldItalic
+ </namerecord>
+ <namerecord nameID="307" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BlackItalic
+ </namerecord>
+ <namerecord nameID="308" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlackItalic
+ </namerecord>
+ <namerecord nameID="309" platformID="3" platEncID="1" langID="0x409">
+ Grade
+ </namerecord>
+ <namerecord nameID="310" platformID="3" platEncID="1" langID="0x409">
+ Normal
+ </namerecord>
+ <namerecord nameID="311" platformID="3" platEncID="1" langID="0x409">
+ X opaque
+ </namerecord>
+ <namerecord nameID="312" platformID="3" platEncID="1" langID="0x409">
+ X transparent
+ </namerecord>
+ <namerecord nameID="313" platformID="3" platEncID="1" langID="0x409">
+ Y opaque
+ </namerecord>
+ <namerecord nameID="314" platformID="3" platEncID="1" langID="0x409">
+ Y transparent ascender
+ </namerecord>
+ <namerecord nameID="315" platformID="3" platEncID="1" langID="0x409">
+ Y transparent descender
+ </namerecord>
+ <namerecord nameID="316" platformID="3" platEncID="1" langID="0x409">
+ Y transparent figures
+ </namerecord>
+ <namerecord nameID="317" platformID="3" platEncID="1" langID="0x409">
+ Y transparent lowercase
+ </namerecord>
+ <namerecord nameID="318" platformID="3" platEncID="1" langID="0x409">
+ Y transparent uppercase
+ </namerecord>
+ <namerecord nameID="319" platformID="3" platEncID="1" langID="0x409">
+ Optical size
+ </namerecord>
+ <namerecord nameID="320" platformID="3" platEncID="1" langID="0x409">
+ 8pt
+ </namerecord>
+ <namerecord nameID="321" platformID="3" platEncID="1" langID="0x409">
+ 9pt
+ </namerecord>
+ <namerecord nameID="322" platformID="3" platEncID="1" langID="0x409">
+ 10pt
+ </namerecord>
+ <namerecord nameID="323" platformID="3" platEncID="1" langID="0x409">
+ 11pt
+ </namerecord>
+ <namerecord nameID="324" platformID="3" platEncID="1" langID="0x409">
+ 12pt
+ </namerecord>
+ <namerecord nameID="325" platformID="3" platEncID="1" langID="0x409">
+ 14pt
+ </namerecord>
+ <namerecord nameID="326" platformID="3" platEncID="1" langID="0x409">
+ 16pt
+ </namerecord>
+ <namerecord nameID="327" platformID="3" platEncID="1" langID="0x409">
+ 17pt
+ </namerecord>
+ <namerecord nameID="328" platformID="3" platEncID="1" langID="0x409">
+ 18pt
+ </namerecord>
+ <namerecord nameID="329" platformID="3" platEncID="1" langID="0x409">
+ 20pt
+ </namerecord>
+ <namerecord nameID="330" platformID="3" platEncID="1" langID="0x409">
+ 24pt
+ </namerecord>
+ <namerecord nameID="331" platformID="3" platEncID="1" langID="0x409">
+ 28pt
+ </namerecord>
+ <namerecord nameID="332" platformID="3" platEncID="1" langID="0x409">
+ 36pt
+ </namerecord>
+ <namerecord nameID="333" platformID="3" platEncID="1" langID="0x409">
+ 48pt
+ </namerecord>
+ <namerecord nameID="334" platformID="3" platEncID="1" langID="0x409">
+ 60pt
+ </namerecord>
+ <namerecord nameID="335" platformID="3" platEncID="1" langID="0x409">
+ 72pt
+ </namerecord>
+ <namerecord nameID="336" platformID="3" platEncID="1" langID="0x409">
+ 96pt
+ </namerecord>
+ <namerecord nameID="337" platformID="3" platEncID="1" langID="0x409">
+ 120pt
+ </namerecord>
+ <namerecord nameID="338" platformID="3" platEncID="1" langID="0x409">
+ 144pt
+ </namerecord>
+ <namerecord nameID="339" platformID="3" platEncID="1" langID="0x409">
+ Width
+ </namerecord>
+ <namerecord nameID="340" platformID="3" platEncID="1" langID="0x409">
+ UltraCondensed
+ </namerecord>
+ <namerecord nameID="341" platformID="3" platEncID="1" langID="0x409">
+ ExtraCondensed
+ </namerecord>
+ <namerecord nameID="342" platformID="3" platEncID="1" langID="0x409">
+ Condensed
+ </namerecord>
+ <namerecord nameID="343" platformID="3" platEncID="1" langID="0x409">
+ SemiCondensed
+ </namerecord>
+ <namerecord nameID="344" platformID="3" platEncID="1" langID="0x409">
+ SemiExpanded
+ </namerecord>
+ <namerecord nameID="345" platformID="3" platEncID="1" langID="0x409">
+ Expanded
+ </namerecord>
+ <namerecord nameID="346" platformID="3" platEncID="1" langID="0x409">
+ ExtraExpanded
+ </namerecord>
+ <namerecord nameID="347" platformID="3" platEncID="1" langID="0x409">
+ Weight
+ </namerecord>
+ <namerecord nameID="348" platformID="3" platEncID="1" langID="0x409">
+ Slant
+ </namerecord>
+ <namerecord nameID="349" platformID="3" platEncID="1" langID="0x409">
+ Default
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-46"/>
+ <underlineThickness value="42"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+ <gasp>
+ <gaspRange rangeMaxPPEM="65535" rangeGaspBehavior="15"/>
+ </gasp>
+
+ <GDEF>
+ <Version value="0x00010002"/>
+ <GlyphClassDef>
+ <ClassDef glyph="I" class="1"/>
+ </GlyphClassDef>
+ <MarkGlyphSetsDef>
+ <MarkSetTableFormat value="1"/>
+ <!-- MarkSetCount=3 -->
+ <Coverage index="0">
+ </Coverage>
+ <Coverage index="1">
+ </Coverage>
+ <Coverage index="2">
+ </Coverage>
+ </MarkGlyphSetsDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="kern"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="2"/>
+ <LookupFlag value="8"/><!-- ignoreMarks -->
+ <!-- SubTableCount=1 -->
+ <PairPos index="0" Format="2">
+ <Coverage>
+ <Glyph value="I"/>
+ </Coverage>
+ <ValueFormat1 value="68"/>
+ <ValueFormat2 value="0"/>
+ <ClassDef1>
+ </ClassDef1>
+ <ClassDef2>
+ <ClassDef glyph="I" class="1"/>
+ </ClassDef2>
+ <!-- Class1Count=1 -->
+ <!-- Class2Count=2 -->
+ <Class1Record index="0">
+ <Class2Record index="0">
+ <Value1 XAdvance="0"/>
+ </Class2Record>
+ <Class2Record index="1">
+ <Value1 XAdvance="0"/>
+ </Class2Record>
+ </Class1Record>
+ </PairPos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=0 -->
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=0 -->
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=0 -->
+ </LookupList>
+ </GSUB>
+
+ <HVAR>
+ <Version value="0x00010000"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=13 -->
+ <!-- RegionCount=25 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="2">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="3">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="4">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="5">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="6">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="7">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="8">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="9">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="10">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="11">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="12">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="13">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="14">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="15">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="16">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="17">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="18">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="19">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="20">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="21">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="22">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="23">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="24">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=2 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=0 -->
+ <Item index="0" value="[]"/>
+ </VarData>
+ <VarData index="1">
+ <!-- ItemCount=1 -->
+ <NumShorts value="7"/>
+ <!-- VarRegionCount=25 -->
+ <VarRegionIndex index="0" value="1"/>
+ <VarRegionIndex index="1" value="5"/>
+ <VarRegionIndex index="2" value="6"/>
+ <VarRegionIndex index="3" value="7"/>
+ <VarRegionIndex index="4" value="14"/>
+ <VarRegionIndex index="5" value="15"/>
+ <VarRegionIndex index="6" value="17"/>
+ <VarRegionIndex index="7" value="0"/>
+ <VarRegionIndex index="8" value="2"/>
+ <VarRegionIndex index="9" value="3"/>
+ <VarRegionIndex index="10" value="4"/>
+ <VarRegionIndex index="11" value="9"/>
+ <VarRegionIndex index="12" value="11"/>
+ <VarRegionIndex index="13" value="12"/>
+ <VarRegionIndex index="14" value="13"/>
+ <VarRegionIndex index="15" value="16"/>
+ <VarRegionIndex index="16" value="18"/>
+ <VarRegionIndex index="17" value="19"/>
+ <VarRegionIndex index="18" value="20"/>
+ <VarRegionIndex index="19" value="21"/>
+ <VarRegionIndex index="20" value="22"/>
+ <VarRegionIndex index="21" value="23"/>
+ <VarRegionIndex index="22" value="24"/>
+ <VarRegionIndex index="23" value="8"/>
+ <VarRegionIndex index="24" value="10"/>
+ <Item index="0" value="[34, -38, -34, 40, 43, -47, 43, -24, -2, 3, 3, 6, 0, 6, 2, 22, 0, -2, 4, -12, -1, 4, -8, 0, 8]"/>
+ </VarData>
+ </VarStore>
+ <AdvWidthMap>
+ <Map glyph=".notdef" outer="0" inner="0"/>
+ <Map glyph="I" outer="1" inner="0"/>
+ </AdvWidthMap>
+ </HVAR>
+
+ <STAT>
+ <Version value="0x00010001"/>
+ <DesignAxisRecordSize value="8"/>
+ <!-- DesignAxisCount=13 -->
+ <DesignAxisRecord>
+ <Axis index="0">
+ <AxisTag value="GRAD"/>
+ <AxisNameID value="309"/> <!-- Grade -->
+ <AxisOrdering value="0"/>
+ </Axis>
+ <Axis index="1">
+ <AxisTag value="XOPQ"/>
+ <AxisNameID value="311"/> <!-- X opaque -->
+ <AxisOrdering value="1"/>
+ </Axis>
+ <Axis index="2">
+ <AxisTag value="XTRA"/>
+ <AxisNameID value="312"/> <!-- X transparent -->
+ <AxisOrdering value="2"/>
+ </Axis>
+ <Axis index="3">
+ <AxisTag value="YOPQ"/>
+ <AxisNameID value="313"/> <!-- Y opaque -->
+ <AxisOrdering value="3"/>
+ </Axis>
+ <Axis index="4">
+ <AxisTag value="YTAS"/>
+ <AxisNameID value="314"/> <!-- Y transparent ascender -->
+ <AxisOrdering value="4"/>
+ </Axis>
+ <Axis index="5">
+ <AxisTag value="YTDE"/>
+ <AxisNameID value="315"/> <!-- Y transparent descender -->
+ <AxisOrdering value="5"/>
+ </Axis>
+ <Axis index="6">
+ <AxisTag value="YTFI"/>
+ <AxisNameID value="316"/> <!-- Y transparent figures -->
+ <AxisOrdering value="6"/>
+ </Axis>
+ <Axis index="7">
+ <AxisTag value="YTLC"/>
+ <AxisNameID value="317"/> <!-- Y transparent lowercase -->
+ <AxisOrdering value="7"/>
+ </Axis>
+ <Axis index="8">
+ <AxisTag value="YTUC"/>
+ <AxisNameID value="318"/> <!-- Y transparent uppercase -->
+ <AxisOrdering value="8"/>
+ </Axis>
+ <Axis index="9">
+ <AxisTag value="opsz"/>
+ <AxisNameID value="319"/> <!-- Optical size -->
+ <AxisOrdering value="9"/>
+ </Axis>
+ <Axis index="10">
+ <AxisTag value="wdth"/>
+ <AxisNameID value="339"/> <!-- Width -->
+ <AxisOrdering value="10"/>
+ </Axis>
+ <Axis index="11">
+ <AxisTag value="wght"/>
+ <AxisNameID value="347"/> <!-- Weight -->
+ <AxisOrdering value="11"/>
+ </Axis>
+ <Axis index="12">
+ <AxisTag value="slnt"/>
+ <AxisNameID value="348"/> <!-- Slant -->
+ <AxisOrdering value="12"/>
+ </Axis>
+ </DesignAxisRecord>
+ <!-- AxisValueCount=46 -->
+ <AxisValueArray>
+ <AxisValue index="0" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="0.0"/>
+ </AxisValue>
+ <AxisValue index="1" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="88.0"/>
+ </AxisValue>
+ <AxisValue index="2" Format="1">
+ <AxisIndex value="2"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="400.0"/>
+ </AxisValue>
+ <AxisValue index="3" Format="1">
+ <AxisIndex value="3"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="116.0"/>
+ </AxisValue>
+ <AxisValue index="4" Format="1">
+ <AxisIndex value="4"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="750.0"/>
+ </AxisValue>
+ <AxisValue index="5" Format="1">
+ <AxisIndex value="5"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="-250.0"/>
+ </AxisValue>
+ <AxisValue index="6" Format="1">
+ <AxisIndex value="6"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="600.0"/>
+ </AxisValue>
+ <AxisValue index="7" Format="1">
+ <AxisIndex value="7"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="500.0"/>
+ </AxisValue>
+ <AxisValue index="8" Format="1">
+ <AxisIndex value="8"/>
+ <Flags value="0"/>
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="725.0"/>
+ </AxisValue>
+ <AxisValue index="9" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="320"/> <!-- 8pt -->
+ <Value value="8.0"/>
+ </AxisValue>
+ <AxisValue index="10" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="321"/> <!-- 9pt -->
+ <Value value="9.0"/>
+ </AxisValue>
+ <AxisValue index="11" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="322"/> <!-- 10pt -->
+ <Value value="10.0"/>
+ </AxisValue>
+ <AxisValue index="12" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="323"/> <!-- 11pt -->
+ <Value value="11.0"/>
+ </AxisValue>
+ <AxisValue index="13" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="324"/> <!-- 12pt -->
+ <Value value="12.0"/>
+ </AxisValue>
+ <AxisValue index="14" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="325"/> <!-- 14pt -->
+ <Value value="14.0"/>
+ </AxisValue>
+ <AxisValue index="15" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="326"/> <!-- 16pt -->
+ <Value value="16.0"/>
+ </AxisValue>
+ <AxisValue index="16" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="327"/> <!-- 17pt -->
+ <Value value="17.0"/>
+ </AxisValue>
+ <AxisValue index="17" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="328"/> <!-- 18pt -->
+ <Value value="18.0"/>
+ </AxisValue>
+ <AxisValue index="18" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="329"/> <!-- 20pt -->
+ <Value value="20.0"/>
+ </AxisValue>
+ <AxisValue index="19" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="330"/> <!-- 24pt -->
+ <Value value="24.0"/>
+ </AxisValue>
+ <AxisValue index="20" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="331"/> <!-- 28pt -->
+ <Value value="28.0"/>
+ </AxisValue>
+ <AxisValue index="21" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="332"/> <!-- 36pt -->
+ <Value value="36.0"/>
+ </AxisValue>
+ <AxisValue index="22" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="333"/> <!-- 48pt -->
+ <Value value="48.0"/>
+ </AxisValue>
+ <AxisValue index="23" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="334"/> <!-- 60pt -->
+ <Value value="60.0"/>
+ </AxisValue>
+ <AxisValue index="24" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="335"/> <!-- 72pt -->
+ <Value value="72.0"/>
+ </AxisValue>
+ <AxisValue index="25" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="336"/> <!-- 96pt -->
+ <Value value="96.0"/>
+ </AxisValue>
+ <AxisValue index="26" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="337"/> <!-- 120pt -->
+ <Value value="120.0"/>
+ </AxisValue>
+ <AxisValue index="27" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="338"/> <!-- 144pt -->
+ <Value value="144.0"/>
+ </AxisValue>
+ <AxisValue index="28" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="340"/> <!-- UltraCondensed -->
+ <Value value="50.0"/>
+ </AxisValue>
+ <AxisValue index="29" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="341"/> <!-- ExtraCondensed -->
+ <Value value="62.5"/>
+ </AxisValue>
+ <AxisValue index="30" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="342"/> <!-- Condensed -->
+ <Value value="75.0"/>
+ </AxisValue>
+ <AxisValue index="31" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="343"/> <!-- SemiCondensed -->
+ <Value value="87.5"/>
+ </AxisValue>
+ <AxisValue index="32" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="33" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="344"/> <!-- SemiExpanded -->
+ <Value value="112.5"/>
+ </AxisValue>
+ <AxisValue index="34" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="345"/> <!-- Expanded -->
+ <Value value="125.0"/>
+ </AxisValue>
+ <AxisValue index="35" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="346"/> <!-- ExtraExpanded -->
+ <Value value="150.0"/>
+ </AxisValue>
+ <AxisValue index="36" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="269"/> <!-- Thin -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="37" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="270"/> <!-- ExtraLight -->
+ <Value value="200.0"/>
+ </AxisValue>
+ <AxisValue index="38" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="271"/> <!-- Light -->
+ <Value value="300.0"/>
+ </AxisValue>
+ <AxisValue index="39" Format="3">
+ <AxisIndex value="11"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="272"/> <!-- Regular -->
+ <Value value="400.0"/>
+ <LinkedValue value="700.0"/>
+ </AxisValue>
+ <AxisValue index="40" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="273"/> <!-- Medium -->
+ <Value value="500.0"/>
+ </AxisValue>
+ <AxisValue index="41" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="274"/> <!-- SemiBold -->
+ <Value value="600.0"/>
+ </AxisValue>
+ <AxisValue index="42" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="275"/> <!-- Bold -->
+ <Value value="700.0"/>
+ </AxisValue>
+ <AxisValue index="43" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="276"/> <!-- ExtraBold -->
+ <Value value="800.0"/>
+ </AxisValue>
+ <AxisValue index="44" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="277"/> <!-- Black -->
+ <Value value="900.0"/>
+ </AxisValue>
+ <AxisValue index="45" Format="1">
+ <AxisIndex value="12"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="349"/> <!-- Default -->
+ <Value value="0.0"/>
+ </AxisValue>
+ </AxisValueArray>
+ <ElidedFallbackNameID value="2"/> <!-- Regular -->
+ </STAT>
+
+ <avar>
+ <segment axis="wght">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="wdth">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="opsz">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="0.16925" to="0.492"/>
+ <mapping from="0.53845" to="0.946"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="GRAD">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="slnt">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="XTRA">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="XOPQ">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YOPQ">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTLC">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTUC">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTAS">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTDE">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTFI">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ </avar>
+
+ <fvar>
+
+ <!-- wght -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>100.0</MinValue>
+ <DefaultValue>400.0</DefaultValue>
+ <MaxValue>1000.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- wdth -->
+ <Axis>
+ <AxisTag>wdth</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>25.0</MinValue>
+ <DefaultValue>100.0</DefaultValue>
+ <MaxValue>151.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+
+ <!-- opsz -->
+ <Axis>
+ <AxisTag>opsz</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>8.0</MinValue>
+ <DefaultValue>14.0</DefaultValue>
+ <MaxValue>144.0</MaxValue>
+ <AxisNameID>258</AxisNameID>
+ </Axis>
+
+ <!-- GRAD -->
+ <Axis>
+ <AxisTag>GRAD</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>-200.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>150.0</MaxValue>
+ <AxisNameID>259</AxisNameID>
+ </Axis>
+
+ <!-- slnt -->
+ <Axis>
+ <AxisTag>slnt</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>-10.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>0.0</MaxValue>
+ <AxisNameID>260</AxisNameID>
+ </Axis>
+
+ <!-- XTRA -->
+ <Axis>
+ <AxisTag>XTRA</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>323.0</MinValue>
+ <DefaultValue>468.0</DefaultValue>
+ <MaxValue>603.0</MaxValue>
+ <AxisNameID>261</AxisNameID>
+ </Axis>
+
+ <!-- XOPQ -->
+ <Axis>
+ <AxisTag>XOPQ</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>27.0</MinValue>
+ <DefaultValue>96.0</DefaultValue>
+ <MaxValue>175.0</MaxValue>
+ <AxisNameID>262</AxisNameID>
+ </Axis>
+
+ <!-- YOPQ -->
+ <Axis>
+ <AxisTag>YOPQ</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>25.0</MinValue>
+ <DefaultValue>79.0</DefaultValue>
+ <MaxValue>135.0</MaxValue>
+ <AxisNameID>263</AxisNameID>
+ </Axis>
+
+ <!-- YTLC -->
+ <Axis>
+ <AxisTag>YTLC</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>416.0</MinValue>
+ <DefaultValue>514.0</DefaultValue>
+ <MaxValue>570.0</MaxValue>
+ <AxisNameID>264</AxisNameID>
+ </Axis>
+
+ <!-- YTUC -->
+ <Axis>
+ <AxisTag>YTUC</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>528.0</MinValue>
+ <DefaultValue>712.0</DefaultValue>
+ <MaxValue>760.0</MaxValue>
+ <AxisNameID>265</AxisNameID>
+ </Axis>
+
+ <!-- YTAS -->
+ <Axis>
+ <AxisTag>YTAS</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>649.0</MinValue>
+ <DefaultValue>750.0</DefaultValue>
+ <MaxValue>854.0</MaxValue>
+ <AxisNameID>266</AxisNameID>
+ </Axis>
+
+ <!-- YTDE -->
+ <Axis>
+ <AxisTag>YTDE</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>-305.0</MinValue>
+ <DefaultValue>-203.0</DefaultValue>
+ <MaxValue>-98.0</MaxValue>
+ <AxisNameID>267</AxisNameID>
+ </Axis>
+
+ <!-- YTFI -->
+ <Axis>
+ <AxisTag>YTFI</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>560.0</MinValue>
+ <DefaultValue>738.0</DefaultValue>
+ <MaxValue>788.0</MaxValue>
+ <AxisNameID>268</AxisNameID>
+ </Axis>
+
+ <!-- Thin -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Thin -->
+ <NamedInstance flags="0x0" postscriptNameID="289" subfamilyNameID="269">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraLight -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLight -->
+ <NamedInstance flags="0x0" postscriptNameID="290" subfamilyNameID="270">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Light -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Light -->
+ <NamedInstance flags="0x0" postscriptNameID="291" subfamilyNameID="271">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Regular -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Regular -->
+ <NamedInstance flags="0x0" postscriptNameID="292" subfamilyNameID="272">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Medium -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Medium -->
+ <NamedInstance flags="0x0" postscriptNameID="293" subfamilyNameID="273">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- SemiBold -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBold -->
+ <NamedInstance flags="0x0" postscriptNameID="294" subfamilyNameID="274">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Bold -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Bold -->
+ <NamedInstance flags="0x0" postscriptNameID="295" subfamilyNameID="275">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBold -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBold -->
+ <NamedInstance flags="0x0" postscriptNameID="296" subfamilyNameID="276">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Black -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Black -->
+ <NamedInstance flags="0x0" postscriptNameID="297" subfamilyNameID="277">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBlack -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlack -->
+ <NamedInstance flags="0x0" postscriptNameID="298" subfamilyNameID="278">
+ <coord axis="wght" value="1000.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Thin Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ThinItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="299" subfamilyNameID="279">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraLight Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="300" subfamilyNameID="280">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Light Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-LightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="301" subfamilyNameID="281">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Italic -->
+ <NamedInstance flags="0x0" postscriptNameID="302" subfamilyNameID="282">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Medium Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-MediumItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="303" subfamilyNameID="283">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- SemiBold Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="304" subfamilyNameID="284">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Bold Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="305" subfamilyNameID="285">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBold Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="306" subfamilyNameID="286">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Black Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="307" subfamilyNameID="287">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBlack Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="308" subfamilyNameID="288">
+ <coord axis="wght" value="1000.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+ </fvar>
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ <glyphVariations glyph="I">
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="-24" y="0"/>
+ <delta pt="2" x="-24" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-24" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="-12" y="0"/>
+ <delta pt="2" x="46" y="0"/>
+ <delta pt="5" x="34" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="-2" y="0"/>
+ <delta pt="2" x="-2" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-2" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="3" y="0"/>
+ <delta pt="2" x="3" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="3" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="3" y="0"/>
+ <delta pt="2" x="3" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="3" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-9" y="0"/>
+ <delta pt="1" x="-29" y="0"/>
+ <delta pt="2" x="-29" y="0"/>
+ <delta pt="3" x="-9" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-38" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="8" y="0"/>
+ <delta pt="1" x="-8" y="0"/>
+ <delta pt="2" x="-8" y="0"/>
+ <delta pt="3" x="8" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="-8" y="0"/>
+ <delta pt="1" x="8" y="0"/>
+ <delta pt="2" x="8" y="0"/>
+ <delta pt="3" x="-8" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="-32" y="0"/>
+ <delta pt="1" x="-32" y="0"/>
+ <delta pt="2" x="32" y="0"/>
+ <delta pt="3" x="32" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="XOPQ" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="2" x="-34" y="0"/>
+ <delta pt="5" x="-34" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="XOPQ" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="2" x="40" y="0"/>
+ <delta pt="5" x="40" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="YTUC" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="-93"/>
+ <delta pt="3" x="0" y="-93"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="YTUC" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="25"/>
+ <delta pt="3" x="0" y="25"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="3" y="0"/>
+ <delta pt="5" x="6" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <delta pt="0" x="3" y="0"/>
+ <delta pt="1" x="5" y="0"/>
+ <delta pt="2" x="5" y="0"/>
+ <delta pt="3" x="3" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="8" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="4" y="0"/>
+ <delta pt="1" x="3" y="0"/>
+ <delta pt="2" x="3" y="0"/>
+ <delta pt="3" x="4" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="6" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="11" y="0"/>
+ <delta pt="1" x="-9" y="0"/>
+ <delta pt="2" x="-9" y="0"/>
+ <delta pt="3" x="11" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="2" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-12" y="0"/>
+ <delta pt="2" x="55" y="0"/>
+ <delta pt="5" x="43" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="-5" y="0"/>
+ <delta pt="1" x="6" y="0"/>
+ <delta pt="2" x="6" y="0"/>
+ <delta pt="3" x="-5" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="6" y="0"/>
+ <delta pt="1" x="-6" y="0"/>
+ <delta pt="2" x="-6" y="0"/>
+ <delta pt="3" x="6" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="6" y="0"/>
+ <delta pt="1" x="-6" y="0"/>
+ <delta pt="2" x="-6" y="0"/>
+ <delta pt="3" x="6" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-23" y="0"/>
+ <delta pt="1" x="-24" y="0"/>
+ <delta pt="2" x="-24" y="0"/>
+ <delta pt="3" x="-23" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-47" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="11" y="0"/>
+ <delta pt="1" x="11" y="0"/>
+ <delta pt="2" x="11" y="0"/>
+ <delta pt="3" x="11" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="22" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="23" y="0"/>
+ <delta pt="1" x="20" y="0"/>
+ <delta pt="2" x="20" y="0"/>
+ <delta pt="3" x="23" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="43" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="-2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-2" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="2" y="0"/>
+ <delta pt="1" x="2" y="0"/>
+ <delta pt="2" x="2" y="0"/>
+ <delta pt="3" x="2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="4" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="-10" y="0"/>
+ <delta pt="2" x="-10" y="0"/>
+ <delta pt="3" x="-2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-12" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="12" y="0"/>
+ <delta pt="1" x="-13" y="0"/>
+ <delta pt="2" x="-13" y="0"/>
+ <delta pt="3" x="12" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-1" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="-5" y="0"/>
+ <delta pt="1" x="9" y="0"/>
+ <delta pt="2" x="9" y="0"/>
+ <delta pt="3" x="-5" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="4" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-3" y="0"/>
+ <delta pt="1" x="-5" y="0"/>
+ <delta pt="2" x="-5" y="0"/>
+ <delta pt="3" x="-3" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-8" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="2" y="0"/>
+ <delta pt="2" x="2" y="0"/>
+ <delta pt="3" x="-2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="1" y="0"/>
+ <delta pt="1" x="-1" y="0"/>
+ <delta pt="2" x="-1" y="0"/>
+ <delta pt="3" x="1" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="-5" y="0"/>
+ <delta pt="1" x="6" y="0"/>
+ <delta pt="2" x="6" y="0"/>
+ <delta pt="3" x="-5" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="1" y="0"/>
+ <delta pt="1" x="-1" y="0"/>
+ <delta pt="2" x="-1" y="0"/>
+ <delta pt="3" x="1" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="1" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="1" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ </gvar>
+
+</ttFont>
diff --git a/Tests/ttLib/data/I.ttf b/Tests/ttLib/data/I.ttf
new file mode 100644
index 00000000..119c24c8
--- /dev/null
+++ b/Tests/ttLib/data/I.ttf
Binary files differ
diff --git a/Tests/ttLib/scaleUpem_test.py b/Tests/ttLib/scaleUpem_test.py
new file mode 100644
index 00000000..dc52bf94
--- /dev/null
+++ b/Tests/ttLib/scaleUpem_test.py
@@ -0,0 +1,75 @@
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.scaleUpem import scale_upem
+import difflib
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+import pytest
+
+class ScaleUpemTest(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = None
+ self.num_tempfiles = 0
+
+ def tearDown(self):
+ if self.tempdir:
+ shutil.rmtree(self.tempdir)
+
+ @staticmethod
+ def get_path(test_file_or_folder):
+ parent_dir = os.path.dirname(__file__)
+ return os.path.join(parent_dir, "data", test_file_or_folder)
+
+ def temp_path(self, suffix):
+ self.temp_dir()
+ self.num_tempfiles += 1
+ return os.path.join(self.tempdir,
+ "tmp%d%s" % (self.num_tempfiles, suffix))
+
+ def temp_dir(self):
+ if not self.tempdir:
+ self.tempdir = tempfile.mkdtemp()
+
+ def read_ttx(self, path):
+ lines = []
+ with open(path, "r", encoding="utf-8") as ttx:
+ for line in ttx.readlines():
+ # Elide ttFont attributes because ttLibVersion may change.
+ if line.startswith("<ttFont "):
+ lines.append("<ttFont>\n")
+ else:
+ lines.append(line.rstrip() + "\n")
+ return lines
+
+ def expect_ttx(self, font, expected_ttx, tables):
+ path = self.temp_path(suffix=".ttx")
+ font.saveXML(path, tables=tables)
+ actual = self.read_ttx(path)
+ expected = self.read_ttx(expected_ttx)
+ if actual != expected:
+ for line in difflib.unified_diff(
+ expected, actual, fromfile=expected_ttx, tofile=path):
+ sys.stdout.write(line)
+ self.fail("TTX output is different from expected")
+
+ def test_scale_upem_ttf(self):
+
+ font = TTFont(self.get_path("I.ttf"))
+ tables = [table_tag for table_tag in font.keys() if table_tag != "head"]
+
+ scale_upem(font, 512)
+
+ expected_ttx_path = self.get_path("I-512upem.ttx")
+ self.expect_ttx(font, expected_ttx_path, tables)
+
+
+ def test_scale_upem_otf(self):
+
+ # Just test that it doesn't crash
+
+ font = TTFont(self.get_path("TestVGID-Regular.otf"))
+
+ scale_upem(font, 500)
diff --git a/Tests/ttLib/tables/C_O_L_R_test.py b/Tests/ttLib/tables/C_O_L_R_test.py
index aaf33003..132449ea 100644
--- a/Tests/ttLib/tables/C_O_L_R_test.py
+++ b/Tests/ttLib/tables/C_O_L_R_test.py
@@ -400,8 +400,8 @@ COLR_V1_XML = [
" </ColorLine>",
' <centerX value="259"/>',
' <centerY value="300"/>',
- ' <startAngle value="45.0"/>',
- ' <endAngle value="135.0"/>',
+ ' <startAngle value="225.0"/>',
+ ' <endAngle value="315.0"/>',
" </Paint>",
' <Glyph value="glyph00011"/>',
" </Paint>",
@@ -532,17 +532,18 @@ COLR_V1_XML = [
COLR_V1_VAR_XML = [
'<VarIndexMap Format="0">',
- ' <Map index="0" outer="1" inner="1"/>',
- ' <Map index="1" outer="1" inner="0"/>',
- ' <Map index="2" outer="1" inner="0"/>',
- ' <Map index="3" outer="1" inner="1"/>',
- ' <Map index="4" outer="1" inner="0"/>',
- ' <Map index="5" outer="1" inner="0"/>',
+ ' <!-- Omitted values default to 0xFFFF/0xFFFF (no variations) -->',
+ ' <Map index="0" outer="1" inner="0"/>',
+ ' <Map index="1"/>',
+ ' <Map index="2"/>',
+ ' <Map index="3" outer="1" inner="0"/>',
+ ' <Map index="4"/>',
+ ' <Map index="5"/>',
' <Map index="6" outer="0" inner="2"/>',
' <Map index="7" outer="0" inner="0"/>',
' <Map index="8" outer="0" inner="1"/>',
- ' <Map index="9" outer="1" inner="0"/>',
- ' <Map index="10" outer="1" inner="0"/>',
+ ' <Map index="9"/>',
+ ' <Map index="10"/>',
' <Map index="11" outer="0" inner="3"/>',
' <Map index="12" outer="0" inner="3"/>',
"</VarIndexMap>",
@@ -571,12 +572,11 @@ COLR_V1_VAR_XML = [
' <Item index="3" value="[500]"/>',
" </VarData>",
' <VarData index="1">',
- " <!-- ItemCount=2 -->",
+ " <!-- ItemCount=1 -->",
' <NumShorts value="32769"/>',
" <!-- VarRegionCount=1 -->",
' <VarRegionIndex index="0" value="0"/>',
- ' <Item index="0" value="[0]"/>',
- ' <Item index="1" value="[65536]"/>',
+ ' <Item index="0" value="[65536]"/>',
" </VarData>",
"</VarStore>",
]
diff --git a/Tests/ttLib/tables/S_V_G__test.py b/Tests/ttLib/tables/S_V_G__test.py
index 91b0f23a..4c40556e 100644
--- a/Tests/ttLib/tables/S_V_G__test.py
+++ b/Tests/ttLib/tables/S_V_G__test.py
@@ -1,3 +1,5 @@
+import gzip
+import io
import struct
from fontTools.misc import etree
@@ -12,6 +14,13 @@ def dump(table, ttFont=None):
print("\n".join(getXML(table.toXML, ttFont)))
+def compress(data: bytes) -> bytes:
+ buf = io.BytesIO()
+ with gzip.GzipFile(None, "w", fileobj=buf, mtime=0) as gz:
+ gz.write(data)
+ return buf.getvalue()
+
+
def strip_xml_whitespace(xml_string):
def strip_or_none(text):
text = text.strip() if text else None
@@ -45,7 +54,9 @@ SVG_DOCS = [
b"""\
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<g id="glyph3">
- <path d="M0,0 L100,0 L50,100 Z"/>
+ <path d="M0,0 L100,0 L50,100 Z" fill="#red"/>
+ <path d="M10,10 L110,10 L60,110 Z" fill="#blue"/>
+ <path d="M20,20 L120,20 L70,120 Z" fill="#green"/>
</g>
</svg>""",
)
@@ -65,25 +76,25 @@ OTSVG_DATA = b"".join(
b"\x00\x02" # endGlyphID (2)
b"\x00\x00\x00\x26" # svgDocOffset (2 + 12*3 == 38 == 0x26)
+ struct.pack(">L", len(SVG_DOCS[0])) # svgDocLength
- # SVGDocumentRecord[1]
+ # SVGDocumentRecord[1] (compressed)
+ b"\x00\x03" # startGlyphID (3)
b"\x00\x03" # endGlyphID (3)
+ struct.pack(">L", 0x26 + len(SVG_DOCS[0])) # svgDocOffset
- + struct.pack(">L", len(SVG_DOCS[1])) # svgDocLength
+ + struct.pack(">L", len(compress(SVG_DOCS[1]))) # svgDocLength
# SVGDocumentRecord[2]
+ b"\x00\x04" # startGlyphID (4)
b"\x00\x04" # endGlyphID (4)
b"\x00\x00\x00\x26" # svgDocOffset (38); records 0 and 2 point to same SVG doc
+ struct.pack(">L", len(SVG_DOCS[0])) # svgDocLength
]
- + SVG_DOCS
+ + [SVG_DOCS[0], compress(SVG_DOCS[1])]
)
OTSVG_TTX = [
'<svgDoc endGlyphID="2" startGlyphID="1">',
f" <![CDATA[{SVG_DOCS[0].decode()}]]>",
"</svgDoc>",
- '<svgDoc endGlyphID="3" startGlyphID="3">',
+ '<svgDoc compressed="1" endGlyphID="3" startGlyphID="3">',
f" <![CDATA[{SVG_DOCS[1].decode()}]]>",
"</svgDoc>",
'<svgDoc endGlyphID="4" startGlyphID="4">',
@@ -129,3 +140,19 @@ def test_round_trip_ttx(font):
table = table_S_V_G_()
table.decompile(compiled, font)
assert getXML(table.toXML, font) == OTSVG_TTX
+
+
+def test_unpack_svg_doc_as_3_tuple():
+ # test that the legacy docList as list of 3-tuples interface still works
+ # even after the new SVGDocument class with extra `compressed` attribute
+ # was added
+ table = table_S_V_G_()
+ table.decompile(OTSVG_DATA, font)
+
+ for doc, compressed in zip(table.docList, (False, True, False)):
+ assert len(doc) == 3
+ data, startGID, endGID = doc
+ assert doc.data == data
+ assert doc.startGlyphID == startGID
+ assert doc.endGlyphID == endGID
+ assert doc.compressed == compressed
diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py
new file mode 100644
index 00000000..bc0bf2ce
--- /dev/null
+++ b/Tests/ttLib/ttGlyphSet_test.py
@@ -0,0 +1,112 @@
+from fontTools.ttLib import TTFont
+from fontTools.ttLib import ttGlyphSet
+from fontTools.pens.recordingPen import RecordingPen
+import os
+import pytest
+
+
+class TTGlyphSetTest(object):
+
+ @staticmethod
+ def getpath(testfile):
+ path = os.path.dirname(__file__)
+ return os.path.join(path, "data", testfile)
+
+ @pytest.mark.parametrize(
+ "location, expected",
+ [
+ (
+ None,
+ [
+ ('moveTo', ((175, 0),)),
+ ('lineTo', ((367, 0),)),
+ ('lineTo', ((367, 1456),)),
+ ('lineTo', ((175, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {},
+ [
+ ('moveTo', ((175, 0),)),
+ ('lineTo', ((367, 0),)),
+ ('lineTo', ((367, 1456),)),
+ ('lineTo', ((175, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 100},
+ [
+ ('moveTo', ((175, 0),)),
+ ('lineTo', ((271, 0),)),
+ ('lineTo', ((271, 1456),)),
+ ('lineTo', ((175, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 1000},
+ [
+ ('moveTo', ((128, 0),)),
+ ('lineTo', ((550, 0),)),
+ ('lineTo', ((550, 1456),)),
+ ('lineTo', ((128, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 1000, 'wdth': 25},
+ [
+ ('moveTo', ((140, 0),)),
+ ('lineTo', ((553, 0),)),
+ ('lineTo', ((553, 1456),)),
+ ('lineTo', ((140, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 1000, 'wdth': 50},
+ [
+ ('moveTo', ((136, 0),)),
+ ('lineTo', ((552, 0),)),
+ ('lineTo', ((552, 1456),)),
+ ('lineTo', ((136, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ ]
+ )
+ def test_glyphset(
+ self, location, expected
+ ):
+ # TODO: also test loading CFF-flavored fonts
+ font = TTFont(self.getpath("I.ttf"))
+ glyphset = font.getGlyphSet(location=location)
+
+ assert isinstance(glyphset, ttGlyphSet._TTGlyphSet)
+ if location:
+ assert isinstance(glyphset, ttGlyphSet._TTVarGlyphSet)
+
+ assert list(glyphset.keys()) == [".notdef", "I"]
+
+ assert "I" in glyphset
+ assert glyphset.has_key("I") # we should really get rid of this...
+
+ assert len(glyphset) == 2
+
+ pen = RecordingPen()
+ glyph = glyphset['I']
+
+ assert glyphset.get("foobar") is None
+
+ assert isinstance(glyph, ttGlyphSet._TTGlyph)
+ if location:
+ assert isinstance(glyph, ttGlyphSet._TTVarGlyphGlyf)
+ else:
+ assert isinstance(glyph, ttGlyphSet._TTGlyphGlyf)
+
+ glyph.draw(pen)
+ actual = pen.value
+
+ assert actual == expected, (location, actual, expected)
diff --git a/Tests/ttLib/ttVisitor_test.py b/Tests/ttLib/ttVisitor_test.py
new file mode 100644
index 00000000..e84e213c
--- /dev/null
+++ b/Tests/ttLib/ttVisitor_test.py
@@ -0,0 +1,39 @@
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.ttVisitor import TTVisitor
+import os
+import pytest
+
+
+class TestVisitor(TTVisitor):
+ def __init__(self):
+ self.value = []
+ self.depth = 0
+
+ def _add(self, s):
+ self.value.append(s)
+
+ def visit(self, obj, target_depth):
+ if self.depth == target_depth:
+ self._add(obj)
+ self.depth += 1
+ super().visit(obj, target_depth)
+ self.depth -= 1
+
+
+class TTVisitorTest(object):
+
+ @staticmethod
+ def getpath(testfile):
+ path = os.path.dirname(__file__)
+ return os.path.join(path, "data", testfile)
+
+ def test_ttvisitor(self):
+
+ font = TTFont(self.getpath("TestVGID-Regular.otf"))
+ visitor = TestVisitor()
+
+ # Count number of objects at depth 1:
+ # That is, number of font tables, including GlyphOrder.
+ visitor.visit(font, 1)
+
+ assert len(visitor.value) == 14
diff --git a/Tests/varLib/data/TestVariableCOLR.designspace b/Tests/varLib/data/TestVariableCOLR.designspace
new file mode 100644
index 00000000..0feddfab
--- /dev/null
+++ b/Tests/varLib/data/TestVariableCOLR.designspace
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="400" maximum="700" default="400"/>
+ </axes>
+ <sources>
+ <source filename="master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx" name="Regular">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ </location>
+ </source>
+ <source filename="master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx" name="Bold">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx
new file mode 100644
index 00000000..4202dabc
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx
@@ -0,0 +1,357 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".space"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="B"/>
+ <GlyphID id="4" name="A.0"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x92daf67f"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Tue Jul 5 12:29:44 2022"/>
+ <modified value="Tue Jul 5 12:29:44 2022"/>
+ <xMin value="51"/>
+ <yMin value="-250"/>
+ <xMax value="878"/>
+ <yMax value="950"/>
+ <macStyle value="00000000 00000001"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="1275"/>
+ <minLeftSideBearing value="51"/>
+ <minRightSideBearing value="397"/>
+ <xMaxExtent value="878"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="5"/>
+ <maxPoints value="16"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="1275"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="666"/>
+ <ySubscriptYSize value="614"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="77"/>
+ <ySuperscriptXSize value="666"/>
+ <ySuperscriptYSize value="614"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="358"/>
+ <yStrikeoutSize value="51"/>
+ <yStrikeoutPosition value="307"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 10100000"/>
+ <usFirstCharIndex value="32"/>
+ <usLastCharIndex value="66"/>
+ <sTypoAscender value="950"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="512"/>
+ <sCapHeight value="717"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="1275" lsb="51"/>
+ <mtx name=".space" width="1275" lsb="0"/>
+ <mtx name="A" width="1275" lsb="0"/>
+ <mtx name="A.0" width="1275" lsb="398"/>
+ <mtx name="B" width="1275" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
+ <contour>
+ <pt x="51" y="-250" on="1"/>
+ <pt x="51" y="950" on="1"/>
+ <pt x="461" y="950" on="1"/>
+ <pt x="461" y="-250" on="1"/>
+ </contour>
+ <contour>
+ <pt x="102" y="-199" on="1"/>
+ <pt x="410" y="-199" on="1"/>
+ <pt x="410" y="899" on="1"/>
+ <pt x="102" y="899" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name=".space"/><!-- contains no outline data -->
+
+ <TTGlyph name="A"/><!-- contains no outline data -->
+
+ <TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
+ <contour>
+ <pt x="878" y="350" on="1"/>
+ <pt x="878" y="416" on="0"/>
+ <pt x="813" y="525" on="0"/>
+ <pt x="704" y="590" on="0"/>
+ <pt x="638" y="590" on="1"/>
+ <pt x="571" y="590" on="0"/>
+ <pt x="462" y="525" on="0"/>
+ <pt x="398" y="416" on="0"/>
+ <pt x="398" y="350" on="1"/>
+ <pt x="398" y="284" on="0"/>
+ <pt x="462" y="175" on="0"/>
+ <pt x="571" y="110" on="0"/>
+ <pt x="638" y="110" on="1"/>
+ <pt x="704" y="110" on="0"/>
+ <pt x="813" y="175" on="0"/>
+ <pt x="878" y="284" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="B"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Bold
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.000;NONE;AnEmojiFamily-Bold
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family Bold
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ AnEmojiFamily-Bold
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-77"/>
+ <underlineThickness value="51"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name=".space"/>
+ <psName name="A.0"/>
+ </extraNames>
+ </post>
+
+ <COLR>
+ <Version value="1"/>
+ <!-- BaseGlyphRecordCount=0 -->
+ <!-- LayerRecordCount=0 -->
+ <BaseGlyphList>
+ <!-- BaseGlyphCount=2 -->
+ <BaseGlyphPaintRecord index="0">
+ <BaseGlyph value="A"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="3"/>
+ <FirstLayerIndex value="0"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ <BaseGlyphPaintRecord index="1">
+ <BaseGlyph value="B"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="2"/>
+ <FirstLayerIndex value="3"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ </BaseGlyphList>
+ <LayerList>
+ <!-- LayerCount=5 -->
+ <Paint index="0" Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="0"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <Paint index="1" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="100"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="2" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ <Paint index="3" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="0.5"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="4" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ </LayerList>
+ <ClipList Format="1">
+ <Clip>
+ <Glyph value="A"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="980"/>
+ <yMax value="600"/>
+ </ClipBox>
+ </Clip>
+ <Clip>
+ <Glyph value="B"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="480"/>
+ </ClipBox>
+ </Clip>
+ </ClipList>
+ </COLR>
+
+ <CPAL>
+ <version value="0"/>
+ <numPaletteEntries value="3"/>
+ <palette index="0">
+ <color index="0" value="#0000FFFF"/>
+ <color index="1" value="#008000FF"/>
+ <color index="2" value="#FF0000FF"/>
+ </palette>
+ </CPAL>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx
new file mode 100644
index 00000000..195be50a
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx
@@ -0,0 +1,335 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".space"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="B"/>
+ <GlyphID id="4" name="A.0"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x49d0234e"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Tue Jul 5 12:29:44 2022"/>
+ <modified value="Tue Jul 5 12:29:44 2022"/>
+ <xMin value="51"/>
+ <yMin value="-250"/>
+ <xMax value="878"/>
+ <yMax value="950"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="1275"/>
+ <minLeftSideBearing value="51"/>
+ <minRightSideBearing value="397"/>
+ <xMaxExtent value="878"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="5"/>
+ <maxPoints value="16"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="1275"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="666"/>
+ <ySubscriptYSize value="614"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="77"/>
+ <ySuperscriptXSize value="666"/>
+ <ySuperscriptYSize value="614"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="358"/>
+ <yStrikeoutSize value="51"/>
+ <yStrikeoutPosition value="307"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 11000000"/>
+ <usFirstCharIndex value="32"/>
+ <usLastCharIndex value="66"/>
+ <sTypoAscender value="950"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="512"/>
+ <sCapHeight value="717"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="1275" lsb="51"/>
+ <mtx name=".space" width="1275" lsb="0"/>
+ <mtx name="A" width="1275" lsb="0"/>
+ <mtx name="A.0" width="1275" lsb="398"/>
+ <mtx name="B" width="1275" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
+ <contour>
+ <pt x="51" y="-250" on="1"/>
+ <pt x="51" y="950" on="1"/>
+ <pt x="461" y="950" on="1"/>
+ <pt x="461" y="-250" on="1"/>
+ </contour>
+ <contour>
+ <pt x="102" y="-199" on="1"/>
+ <pt x="410" y="-199" on="1"/>
+ <pt x="410" y="899" on="1"/>
+ <pt x="102" y="899" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name=".space"/><!-- contains no outline data -->
+
+ <TTGlyph name="A"/><!-- contains no outline data -->
+
+ <TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
+ <contour>
+ <pt x="878" y="350" on="1"/>
+ <pt x="878" y="416" on="0"/>
+ <pt x="813" y="525" on="0"/>
+ <pt x="704" y="590" on="0"/>
+ <pt x="638" y="590" on="1"/>
+ <pt x="571" y="590" on="0"/>
+ <pt x="462" y="525" on="0"/>
+ <pt x="398" y="416" on="0"/>
+ <pt x="398" y="350" on="1"/>
+ <pt x="398" y="284" on="0"/>
+ <pt x="462" y="175" on="0"/>
+ <pt x="571" y="110" on="0"/>
+ <pt x="638" y="110" on="1"/>
+ <pt x="704" y="110" on="0"/>
+ <pt x="813" y="175" on="0"/>
+ <pt x="878" y="284" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="B"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.000;NONE;AnEmojiFamily-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ AnEmojiFamily-Regular
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-77"/>
+ <underlineThickness value="51"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name=".space"/>
+ <psName name="A.0"/>
+ </extraNames>
+ </post>
+
+ <COLR>
+ <Version value="1"/>
+ <!-- BaseGlyphRecordCount=0 -->
+ <!-- LayerRecordCount=0 -->
+ <BaseGlyphList>
+ <!-- BaseGlyphCount=2 -->
+ <BaseGlyphPaintRecord index="0">
+ <BaseGlyph value="A"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="3"/>
+ <FirstLayerIndex value="0"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ <BaseGlyphPaintRecord index="1">
+ <BaseGlyph value="B"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="2"/>
+ <FirstLayerIndex value="1"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ </BaseGlyphList>
+ <LayerList>
+ <!-- LayerCount=3 -->
+ <Paint index="0" Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="0"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <Paint index="1" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="2" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ </LayerList>
+ <ClipList Format="1">
+ <Clip>
+ <Glyph value="A"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="600"/>
+ </ClipBox>
+ </Clip>
+ <Clip>
+ <Glyph value="B"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="480"/>
+ </ClipBox>
+ </Clip>
+ </ClipList>
+ </COLR>
+
+ <CPAL>
+ <version value="0"/>
+ <numPaletteEntries value="3"/>
+ <palette index="0">
+ <color index="0" value="#0000FFFF"/>
+ <color index="1" value="#008000FF"/>
+ <color index="2" value="#FF0000FF"/>
+ </palette>
+ </CPAL>
+
+</ttFont>
diff --git a/Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx b/Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx
new file mode 100644
index 00000000..8d0177ab
--- /dev/null
+++ b/Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".space"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="B"/>
+ <GlyphID id="4" name="A.0"/>
+ </GlyphOrder>
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>400.0</MinValue>
+ <DefaultValue>400.0</DefaultValue>
+ <MaxValue>700.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+ </fvar>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
+ <contour>
+ <pt x="51" y="-250" on="1"/>
+ <pt x="51" y="950" on="1"/>
+ <pt x="461" y="950" on="1"/>
+ <pt x="461" y="-250" on="1"/>
+ </contour>
+ <contour>
+ <pt x="102" y="-199" on="1"/>
+ <pt x="410" y="-199" on="1"/>
+ <pt x="410" y="899" on="1"/>
+ <pt x="102" y="899" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name=".space"/><!-- contains no outline data -->
+
+ <TTGlyph name="A"/><!-- contains no outline data -->
+
+ <TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
+ <contour>
+ <pt x="878" y="350" on="1"/>
+ <pt x="878" y="416" on="0"/>
+ <pt x="813" y="525" on="0"/>
+ <pt x="704" y="590" on="0"/>
+ <pt x="638" y="590" on="1"/>
+ <pt x="571" y="590" on="0"/>
+ <pt x="462" y="525" on="0"/>
+ <pt x="398" y="416" on="0"/>
+ <pt x="398" y="350" on="1"/>
+ <pt x="398" y="284" on="0"/>
+ <pt x="462" y="175" on="0"/>
+ <pt x="571" y="110" on="0"/>
+ <pt x="638" y="110" on="1"/>
+ <pt x="704" y="110" on="0"/>
+ <pt x="813" y="175" on="0"/>
+ <pt x="878" y="284" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="B"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <COLR>
+ <Version value="1"/>
+ <!-- BaseGlyphRecordCount=0 -->
+ <!-- LayerRecordCount=0 -->
+ <BaseGlyphList>
+ <!-- BaseGlyphCount=2 -->
+ <BaseGlyphPaintRecord index="0">
+ <BaseGlyph value="A"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="3"/>
+ <FirstLayerIndex value="0"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ <BaseGlyphPaintRecord index="1">
+ <BaseGlyph value="B"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="2"/>
+ <FirstLayerIndex value="3"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ </BaseGlyphList>
+ <LayerList>
+ <!-- LayerCount=5 -->
+ <Paint index="0" Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="0"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <Paint index="1" Format="15"><!-- PaintVarTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-120"/>
+ <VarIndexBase value="0"/>
+ </Paint>
+ <Paint index="2" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ <Paint index="3" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="3"><!-- PaintVarSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="1.0"/>
+ <VarIndexBase value="2"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="4" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ </LayerList>
+ <ClipList Format="1">
+ <Clip>
+ <Glyph value="A"/>
+ <ClipBox Format="2">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="600"/>
+ <VarIndexBase value="3"/>
+ </ClipBox>
+ </Clip>
+ <Clip>
+ <Glyph value="B"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="480"/>
+ </ClipBox>
+ </Clip>
+ </ClipList>
+ <VarIndexMap Format="0">
+ <!-- Omitted values default to 0xFFFF/0xFFFF (no variations) -->
+ <Map index="0" outer="0" inner="1"/>
+ <Map index="1"/>
+ <Map index="2" outer="0" inner="0"/>
+ <Map index="3"/>
+ <Map index="4"/>
+ <Map index="5" outer="0" inner="1"/>
+ <Map index="6"/>
+ </VarIndexMap>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=1 -->
+ <!-- RegionCount=1 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=2 -->
+ <NumShorts value="1"/>
+ <!-- VarRegionCount=1 -->
+ <VarRegionIndex index="0" value="0"/>
+ <Item index="0" value="[-8192]"/>
+ <Item index="1" value="[100]"/>
+ </VarData>
+ </VarStore>
+ </COLR>
+
+ <CPAL>
+ <version value="0"/>
+ <numPaletteEntries value="3"/>
+ <palette index="0">
+ <color index="0" value="#0000FFFF"/>
+ <color index="1" value="#008000FF"/>
+ <color index="2" value="#FF0000FF"/>
+ </palette>
+ </CPAL>
+
+</ttFont>
diff --git a/Tests/varLib/instancer/data/STATInstancerTest.ttx b/Tests/varLib/instancer/data/STATInstancerTest.ttx
new file mode 100644
index 00000000..eee24d82
--- /dev/null
+++ b/Tests/varLib/instancer/data/STATInstancerTest.ttx
@@ -0,0 +1,1830 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="0.0"/>
+ <checkSumAdjustment value="0xbc466984"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Tue Jul 5 13:33:16 2022"/>
+ <modified value="Tue Jul 5 13:33:42 2022"/>
+ <xMin value="50"/>
+ <yMin value="-200"/>
+ <xMax value="450"/>
+ <yMax value="800"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="1000"/>
+ <descent value="-200"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="500"/>
+ <minLeftSideBearing value="50"/>
+ <minRightSideBearing value="50"/>
+ <xMaxExtent value="450"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="1"/>
+ <maxPoints value="8"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="500"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="650"/>
+ <ySubscriptYSize value="600"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="75"/>
+ <ySuperscriptXSize value="650"/>
+ <ySuperscriptYSize value="600"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="350"/>
+ <yStrikeoutSize value="50"/>
+ <yStrikeoutPosition value="300"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="65535"/>
+ <usLastCharIndex value="65535"/>
+ <sTypoAscender value="800"/>
+ <sTypoDescender value="-200"/>
+ <sTypoLineGap value="200"/>
+ <usWinAscent value="1000"/>
+ <usWinDescent value="200"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="500"/>
+ <sCapHeight value="700"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="50"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="50" yMin="-200" xMax="450" yMax="800">
+ <contour>
+ <pt x="50" y="-200" on="1"/>
+ <pt x="50" y="800" on="1"/>
+ <pt x="450" y="800" on="1"/>
+ <pt x="450" y="-200" on="1"/>
+ </contour>
+ <contour>
+ <pt x="100" y="-150" on="1"/>
+ <pt x="400" y="-150" on="1"/>
+ <pt x="400" y="750" on="1"/>
+ <pt x="100" y="750" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Weight
+ </namerecord>
+ <namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Width
+ </namerecord>
+ <namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Italic
+ </namerecord>
+ <namerecord nameID="259" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Hair
+ </namerecord>
+ <namerecord nameID="260" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdHair
+ </namerecord>
+ <namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Hair Italic
+ </namerecord>
+ <namerecord nameID="262" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdHairItalic
+ </namerecord>
+ <namerecord nameID="263" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Hair
+ </namerecord>
+ <namerecord nameID="264" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Hair
+ </namerecord>
+ <namerecord nameID="265" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Hair Italic
+ </namerecord>
+ <namerecord nameID="266" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-HairItalic
+ </namerecord>
+ <namerecord nameID="267" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Hair
+ </namerecord>
+ <namerecord nameID="268" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExHair
+ </namerecord>
+ <namerecord nameID="269" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Hair Italic
+ </namerecord>
+ <namerecord nameID="270" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExHairItalic
+ </namerecord>
+ <namerecord nameID="271" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Thin
+ </namerecord>
+ <namerecord nameID="272" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdThin
+ </namerecord>
+ <namerecord nameID="273" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Thin Italic
+ </namerecord>
+ <namerecord nameID="274" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdThinItalic
+ </namerecord>
+ <namerecord nameID="275" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Thin
+ </namerecord>
+ <namerecord nameID="276" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Thin
+ </namerecord>
+ <namerecord nameID="277" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Thin Italic
+ </namerecord>
+ <namerecord nameID="278" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ThinItalic
+ </namerecord>
+ <namerecord nameID="279" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Thin
+ </namerecord>
+ <namerecord nameID="280" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExThin
+ </namerecord>
+ <namerecord nameID="281" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Thin Italic
+ </namerecord>
+ <namerecord nameID="282" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExThinItalic
+ </namerecord>
+ <namerecord nameID="283" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Light
+ </namerecord>
+ <namerecord nameID="284" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdLight
+ </namerecord>
+ <namerecord nameID="285" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Light Italic
+ </namerecord>
+ <namerecord nameID="286" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdLightItalic
+ </namerecord>
+ <namerecord nameID="287" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Light
+ </namerecord>
+ <namerecord nameID="288" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Light
+ </namerecord>
+ <namerecord nameID="289" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Light Italic
+ </namerecord>
+ <namerecord nameID="290" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-LightItalic
+ </namerecord>
+ <namerecord nameID="291" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Light
+ </namerecord>
+ <namerecord nameID="292" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExLight
+ </namerecord>
+ <namerecord nameID="293" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Light Italic
+ </namerecord>
+ <namerecord nameID="294" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExLightItalic
+ </namerecord>
+ <namerecord nameID="295" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd
+ </namerecord>
+ <namerecord nameID="296" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Cd
+ </namerecord>
+ <namerecord nameID="297" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Italic
+ </namerecord>
+ <namerecord nameID="298" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdItalic
+ </namerecord>
+ <namerecord nameID="299" platformID="1" platEncID="0" langID="0x0" unicode="True">
+
+ </namerecord>
+ <namerecord nameID="300" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-
+ </namerecord>
+ <namerecord nameID="301" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Italic
+ </namerecord>
+ <namerecord nameID="302" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex
+ </namerecord>
+ <namerecord nameID="303" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Ex
+ </namerecord>
+ <namerecord nameID="304" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Italic
+ </namerecord>
+ <namerecord nameID="305" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExItalic
+ </namerecord>
+ <namerecord nameID="306" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Medium
+ </namerecord>
+ <namerecord nameID="307" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdMedium
+ </namerecord>
+ <namerecord nameID="308" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Medium Italic
+ </namerecord>
+ <namerecord nameID="309" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdMediumItalic
+ </namerecord>
+ <namerecord nameID="310" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Medium
+ </namerecord>
+ <namerecord nameID="311" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Medium
+ </namerecord>
+ <namerecord nameID="312" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Medium Italic
+ </namerecord>
+ <namerecord nameID="313" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-MediumItalic
+ </namerecord>
+ <namerecord nameID="314" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Medium
+ </namerecord>
+ <namerecord nameID="315" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExMedium
+ </namerecord>
+ <namerecord nameID="316" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Medium Italic
+ </namerecord>
+ <namerecord nameID="317" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExMediumItalic
+ </namerecord>
+ <namerecord nameID="318" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd SemiBold
+ </namerecord>
+ <namerecord nameID="319" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdSemiBold
+ </namerecord>
+ <namerecord nameID="320" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd SemiBold Italic
+ </namerecord>
+ <namerecord nameID="321" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdSemiBoldItalic
+ </namerecord>
+ <namerecord nameID="322" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ SemiBold
+ </namerecord>
+ <namerecord nameID="323" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-SemiBold
+ </namerecord>
+ <namerecord nameID="324" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ SemiBold Italic
+ </namerecord>
+ <namerecord nameID="325" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-SemiBoldItalic
+ </namerecord>
+ <namerecord nameID="326" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex SemiBold
+ </namerecord>
+ <namerecord nameID="327" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExSemiBold
+ </namerecord>
+ <namerecord nameID="328" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex SemiBold Italic
+ </namerecord>
+ <namerecord nameID="329" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExSemiBoldItalic
+ </namerecord>
+ <namerecord nameID="330" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Bold
+ </namerecord>
+ <namerecord nameID="331" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdBold
+ </namerecord>
+ <namerecord nameID="332" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Bold Italic
+ </namerecord>
+ <namerecord nameID="333" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdBoldItalic
+ </namerecord>
+ <namerecord nameID="334" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Bold
+ </namerecord>
+ <namerecord nameID="335" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Bold
+ </namerecord>
+ <namerecord nameID="336" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Bold Italic
+ </namerecord>
+ <namerecord nameID="337" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-BoldItalic
+ </namerecord>
+ <namerecord nameID="338" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Bold
+ </namerecord>
+ <namerecord nameID="339" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExBold
+ </namerecord>
+ <namerecord nameID="340" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Bold Italic
+ </namerecord>
+ <namerecord nameID="341" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExBoldItalic
+ </namerecord>
+ <namerecord nameID="342" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd XBold
+ </namerecord>
+ <namerecord nameID="343" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdXBold
+ </namerecord>
+ <namerecord nameID="344" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd XBold Italic
+ </namerecord>
+ <namerecord nameID="345" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdXBoldItalic
+ </namerecord>
+ <namerecord nameID="346" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ XBold
+ </namerecord>
+ <namerecord nameID="347" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-XBold
+ </namerecord>
+ <namerecord nameID="348" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ XBold Italic
+ </namerecord>
+ <namerecord nameID="349" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-XBoldItalic
+ </namerecord>
+ <namerecord nameID="350" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex XBold
+ </namerecord>
+ <namerecord nameID="351" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExXBold
+ </namerecord>
+ <namerecord nameID="352" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex XBold Italic
+ </namerecord>
+ <namerecord nameID="353" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExXBoldItalic
+ </namerecord>
+ <namerecord nameID="354" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Black
+ </namerecord>
+ <namerecord nameID="355" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdBlack
+ </namerecord>
+ <namerecord nameID="356" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Cd Black Italic
+ </namerecord>
+ <namerecord nameID="357" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-CdBlackItalic
+ </namerecord>
+ <namerecord nameID="358" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Black
+ </namerecord>
+ <namerecord nameID="359" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-Black
+ </namerecord>
+ <namerecord nameID="360" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Black Italic
+ </namerecord>
+ <namerecord nameID="361" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-BlackItalic
+ </namerecord>
+ <namerecord nameID="362" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Black
+ </namerecord>
+ <namerecord nameID="363" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExBlack
+ </namerecord>
+ <namerecord nameID="364" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Ex Black Italic
+ </namerecord>
+ <namerecord nameID="365" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ NewFont-ExBlackItalic
+ </namerecord>
+ <namerecord nameID="366" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Regular
+ </namerecord>
+ <namerecord nameID="367" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Normal
+ </namerecord>
+ <namerecord nameID="368" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Upright
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ New Font
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 0.000;NONE;NewFont-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ New Font Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 0.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Regular
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ Weight
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
+ Width
+ </namerecord>
+ <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
+ Italic
+ </namerecord>
+ <namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
+ Cd Hair
+ </namerecord>
+ <namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdHair
+ </namerecord>
+ <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
+ Cd Hair Italic
+ </namerecord>
+ <namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdHairItalic
+ </namerecord>
+ <namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
+ Hair
+ </namerecord>
+ <namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Hair
+ </namerecord>
+ <namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
+ Hair Italic
+ </namerecord>
+ <namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
+ NewFont-HairItalic
+ </namerecord>
+ <namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
+ Ex Hair
+ </namerecord>
+ <namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExHair
+ </namerecord>
+ <namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
+ Ex Hair Italic
+ </namerecord>
+ <namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExHairItalic
+ </namerecord>
+ <namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
+ Cd Thin
+ </namerecord>
+ <namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdThin
+ </namerecord>
+ <namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
+ Cd Thin Italic
+ </namerecord>
+ <namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdThinItalic
+ </namerecord>
+ <namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
+ Thin
+ </namerecord>
+ <namerecord nameID="276" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Thin
+ </namerecord>
+ <namerecord nameID="277" platformID="3" platEncID="1" langID="0x409">
+ Thin Italic
+ </namerecord>
+ <namerecord nameID="278" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ThinItalic
+ </namerecord>
+ <namerecord nameID="279" platformID="3" platEncID="1" langID="0x409">
+ Ex Thin
+ </namerecord>
+ <namerecord nameID="280" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExThin
+ </namerecord>
+ <namerecord nameID="281" platformID="3" platEncID="1" langID="0x409">
+ Ex Thin Italic
+ </namerecord>
+ <namerecord nameID="282" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExThinItalic
+ </namerecord>
+ <namerecord nameID="283" platformID="3" platEncID="1" langID="0x409">
+ Cd Light
+ </namerecord>
+ <namerecord nameID="284" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdLight
+ </namerecord>
+ <namerecord nameID="285" platformID="3" platEncID="1" langID="0x409">
+ Cd Light Italic
+ </namerecord>
+ <namerecord nameID="286" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdLightItalic
+ </namerecord>
+ <namerecord nameID="287" platformID="3" platEncID="1" langID="0x409">
+ Light
+ </namerecord>
+ <namerecord nameID="288" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Light
+ </namerecord>
+ <namerecord nameID="289" platformID="3" platEncID="1" langID="0x409">
+ Light Italic
+ </namerecord>
+ <namerecord nameID="290" platformID="3" platEncID="1" langID="0x409">
+ NewFont-LightItalic
+ </namerecord>
+ <namerecord nameID="291" platformID="3" platEncID="1" langID="0x409">
+ Ex Light
+ </namerecord>
+ <namerecord nameID="292" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExLight
+ </namerecord>
+ <namerecord nameID="293" platformID="3" platEncID="1" langID="0x409">
+ Ex Light Italic
+ </namerecord>
+ <namerecord nameID="294" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExLightItalic
+ </namerecord>
+ <namerecord nameID="295" platformID="3" platEncID="1" langID="0x409">
+ Cd
+ </namerecord>
+ <namerecord nameID="296" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Cd
+ </namerecord>
+ <namerecord nameID="297" platformID="3" platEncID="1" langID="0x409">
+ Cd Italic
+ </namerecord>
+ <namerecord nameID="298" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdItalic
+ </namerecord>
+ <namerecord nameID="299" platformID="3" platEncID="1" langID="0x409">
+
+ </namerecord>
+ <namerecord nameID="300" platformID="3" platEncID="1" langID="0x409">
+ NewFont-
+ </namerecord>
+ <namerecord nameID="301" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Italic
+ </namerecord>
+ <namerecord nameID="302" platformID="3" platEncID="1" langID="0x409">
+ Ex
+ </namerecord>
+ <namerecord nameID="303" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Ex
+ </namerecord>
+ <namerecord nameID="304" platformID="3" platEncID="1" langID="0x409">
+ Ex Italic
+ </namerecord>
+ <namerecord nameID="305" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExItalic
+ </namerecord>
+ <namerecord nameID="306" platformID="3" platEncID="1" langID="0x409">
+ Cd Medium
+ </namerecord>
+ <namerecord nameID="307" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdMedium
+ </namerecord>
+ <namerecord nameID="308" platformID="3" platEncID="1" langID="0x409">
+ Cd Medium Italic
+ </namerecord>
+ <namerecord nameID="309" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdMediumItalic
+ </namerecord>
+ <namerecord nameID="310" platformID="3" platEncID="1" langID="0x409">
+ Medium
+ </namerecord>
+ <namerecord nameID="311" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Medium
+ </namerecord>
+ <namerecord nameID="312" platformID="3" platEncID="1" langID="0x409">
+ Medium Italic
+ </namerecord>
+ <namerecord nameID="313" platformID="3" platEncID="1" langID="0x409">
+ NewFont-MediumItalic
+ </namerecord>
+ <namerecord nameID="314" platformID="3" platEncID="1" langID="0x409">
+ Ex Medium
+ </namerecord>
+ <namerecord nameID="315" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExMedium
+ </namerecord>
+ <namerecord nameID="316" platformID="3" platEncID="1" langID="0x409">
+ Ex Medium Italic
+ </namerecord>
+ <namerecord nameID="317" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExMediumItalic
+ </namerecord>
+ <namerecord nameID="318" platformID="3" platEncID="1" langID="0x409">
+ Cd SemiBold
+ </namerecord>
+ <namerecord nameID="319" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdSemiBold
+ </namerecord>
+ <namerecord nameID="320" platformID="3" platEncID="1" langID="0x409">
+ Cd SemiBold Italic
+ </namerecord>
+ <namerecord nameID="321" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdSemiBoldItalic
+ </namerecord>
+ <namerecord nameID="322" platformID="3" platEncID="1" langID="0x409">
+ SemiBold
+ </namerecord>
+ <namerecord nameID="323" platformID="3" platEncID="1" langID="0x409">
+ NewFont-SemiBold
+ </namerecord>
+ <namerecord nameID="324" platformID="3" platEncID="1" langID="0x409">
+ SemiBold Italic
+ </namerecord>
+ <namerecord nameID="325" platformID="3" platEncID="1" langID="0x409">
+ NewFont-SemiBoldItalic
+ </namerecord>
+ <namerecord nameID="326" platformID="3" platEncID="1" langID="0x409">
+ Ex SemiBold
+ </namerecord>
+ <namerecord nameID="327" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExSemiBold
+ </namerecord>
+ <namerecord nameID="328" platformID="3" platEncID="1" langID="0x409">
+ Ex SemiBold Italic
+ </namerecord>
+ <namerecord nameID="329" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExSemiBoldItalic
+ </namerecord>
+ <namerecord nameID="330" platformID="3" platEncID="1" langID="0x409">
+ Cd Bold
+ </namerecord>
+ <namerecord nameID="331" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdBold
+ </namerecord>
+ <namerecord nameID="332" platformID="3" platEncID="1" langID="0x409">
+ Cd Bold Italic
+ </namerecord>
+ <namerecord nameID="333" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdBoldItalic
+ </namerecord>
+ <namerecord nameID="334" platformID="3" platEncID="1" langID="0x409">
+ Bold
+ </namerecord>
+ <namerecord nameID="335" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Bold
+ </namerecord>
+ <namerecord nameID="336" platformID="3" platEncID="1" langID="0x409">
+ Bold Italic
+ </namerecord>
+ <namerecord nameID="337" platformID="3" platEncID="1" langID="0x409">
+ NewFont-BoldItalic
+ </namerecord>
+ <namerecord nameID="338" platformID="3" platEncID="1" langID="0x409">
+ Ex Bold
+ </namerecord>
+ <namerecord nameID="339" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExBold
+ </namerecord>
+ <namerecord nameID="340" platformID="3" platEncID="1" langID="0x409">
+ Ex Bold Italic
+ </namerecord>
+ <namerecord nameID="341" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExBoldItalic
+ </namerecord>
+ <namerecord nameID="342" platformID="3" platEncID="1" langID="0x409">
+ Cd XBold
+ </namerecord>
+ <namerecord nameID="343" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdXBold
+ </namerecord>
+ <namerecord nameID="344" platformID="3" platEncID="1" langID="0x409">
+ Cd XBold Italic
+ </namerecord>
+ <namerecord nameID="345" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdXBoldItalic
+ </namerecord>
+ <namerecord nameID="346" platformID="3" platEncID="1" langID="0x409">
+ XBold
+ </namerecord>
+ <namerecord nameID="347" platformID="3" platEncID="1" langID="0x409">
+ NewFont-XBold
+ </namerecord>
+ <namerecord nameID="348" platformID="3" platEncID="1" langID="0x409">
+ XBold Italic
+ </namerecord>
+ <namerecord nameID="349" platformID="3" platEncID="1" langID="0x409">
+ NewFont-XBoldItalic
+ </namerecord>
+ <namerecord nameID="350" platformID="3" platEncID="1" langID="0x409">
+ Ex XBold
+ </namerecord>
+ <namerecord nameID="351" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExXBold
+ </namerecord>
+ <namerecord nameID="352" platformID="3" platEncID="1" langID="0x409">
+ Ex XBold Italic
+ </namerecord>
+ <namerecord nameID="353" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExXBoldItalic
+ </namerecord>
+ <namerecord nameID="354" platformID="3" platEncID="1" langID="0x409">
+ Cd Black
+ </namerecord>
+ <namerecord nameID="355" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdBlack
+ </namerecord>
+ <namerecord nameID="356" platformID="3" platEncID="1" langID="0x409">
+ Cd Black Italic
+ </namerecord>
+ <namerecord nameID="357" platformID="3" platEncID="1" langID="0x409">
+ NewFont-CdBlackItalic
+ </namerecord>
+ <namerecord nameID="358" platformID="3" platEncID="1" langID="0x409">
+ Black
+ </namerecord>
+ <namerecord nameID="359" platformID="3" platEncID="1" langID="0x409">
+ NewFont-Black
+ </namerecord>
+ <namerecord nameID="360" platformID="3" platEncID="1" langID="0x409">
+ Black Italic
+ </namerecord>
+ <namerecord nameID="361" platformID="3" platEncID="1" langID="0x409">
+ NewFont-BlackItalic
+ </namerecord>
+ <namerecord nameID="362" platformID="3" platEncID="1" langID="0x409">
+ Ex Black
+ </namerecord>
+ <namerecord nameID="363" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExBlack
+ </namerecord>
+ <namerecord nameID="364" platformID="3" platEncID="1" langID="0x409">
+ Ex Black Italic
+ </namerecord>
+ <namerecord nameID="365" platformID="3" platEncID="1" langID="0x409">
+ NewFont-ExBlackItalic
+ </namerecord>
+ <namerecord nameID="366" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="367" platformID="3" platEncID="1" langID="0x409">
+ Normal
+ </namerecord>
+ <namerecord nameID="368" platformID="3" platEncID="1" langID="0x409">
+ Upright
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-75"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ </extraNames>
+ </post>
+
+ <HVAR>
+ <Version value="0x00010000"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=3 -->
+ <!-- RegionCount=19 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.48517"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="2">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.48517"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="3">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="4">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="5">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="6">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="7">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="8">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="9">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="10">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="11">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.48517"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="12">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.48517"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="13">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="14">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="15">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="16">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="17">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="18">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=1 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=0 -->
+ <Item index="0" value="[]"/>
+ </VarData>
+ </VarStore>
+ </HVAR>
+
+ <STAT>
+ <Version value="0x00010001"/>
+ <DesignAxisRecordSize value="8"/>
+ <!-- DesignAxisCount=3 -->
+ <DesignAxisRecord>
+ <Axis index="0">
+ <AxisTag value="wght"/>
+ <AxisNameID value="256"/> <!-- Weight -->
+ <AxisOrdering value="1"/>
+ </Axis>
+ <Axis index="1">
+ <AxisTag value="wdth"/>
+ <AxisNameID value="257"/> <!-- Width -->
+ <AxisOrdering value="0"/>
+ </Axis>
+ <Axis index="2">
+ <AxisTag value="ital"/>
+ <AxisNameID value="258"/> <!-- Italic -->
+ <AxisOrdering value="2"/>
+ </Axis>
+ </DesignAxisRecord>
+ <!-- AxisValueCount=14 -->
+ <AxisValueArray>
+ <AxisValue index="0" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="263"/> <!-- Hair -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="1" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="275"/> <!-- Thin -->
+ <Value value="200.0"/>
+ </AxisValue>
+ <AxisValue index="2" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="287"/> <!-- Light -->
+ <Value value="300.0"/>
+ </AxisValue>
+ <AxisValue index="3" Format="3">
+ <AxisIndex value="0"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="366"/> <!-- Regular -->
+ <Value value="400.0"/>
+ <LinkedValue value="700.0"/>
+ </AxisValue>
+ <AxisValue index="4" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="310"/> <!-- Medium -->
+ <Value value="500.0"/>
+ </AxisValue>
+ <AxisValue index="5" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="322"/> <!-- SemiBold -->
+ <Value value="600.0"/>
+ </AxisValue>
+ <AxisValue index="6" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="334"/> <!-- Bold -->
+ <Value value="700.0"/>
+ </AxisValue>
+ <AxisValue index="7" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="346"/> <!-- XBold -->
+ <Value value="800.0"/>
+ </AxisValue>
+ <AxisValue index="8" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="358"/> <!-- Black -->
+ <Value value="900.0"/>
+ </AxisValue>
+ <AxisValue index="9" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="0"/>
+ <ValueNameID value="295"/> <!-- Cd -->
+ <Value value="75.0"/>
+ </AxisValue>
+ <AxisValue index="10" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="367"/> <!-- Normal -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="11" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="0"/>
+ <ValueNameID value="302"/> <!-- Ex -->
+ <Value value="125.0"/>
+ </AxisValue>
+ <AxisValue index="12" Format="3">
+ <AxisIndex value="2"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="368"/> <!-- Upright -->
+ <Value value="0.0"/>
+ <LinkedValue value="1.0"/>
+ </AxisValue>
+ <AxisValue index="13" Format="1">
+ <AxisIndex value="2"/>
+ <Flags value="0"/>
+ <ValueNameID value="258"/> <!-- Italic -->
+ <Value value="1.0"/>
+ </AxisValue>
+ </AxisValueArray>
+ <ElidedFallbackNameID value="2"/> <!-- Regular -->
+ </STAT>
+
+ <avar>
+ <segment axis="wght">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="-0.6667" to="-0.74194"/>
+ <mapping from="-0.3333" to="-0.4355"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="0.2" to="0.1386"/>
+ <mapping from="0.4" to="0.30695"/>
+ <mapping from="0.6" to="0.48517"/>
+ <mapping from="0.8" to="0.73267"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="wdth">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="ital">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ </avar>
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>100.0</MinValue>
+ <DefaultValue>400.0</DefaultValue>
+ <MaxValue>900.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- Width -->
+ <Axis>
+ <AxisTag>wdth</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>75.0</MinValue>
+ <DefaultValue>100.0</DefaultValue>
+ <MaxValue>125.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+
+ <!-- Italic -->
+ <Axis>
+ <AxisTag>ital</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>1.0</MaxValue>
+ <AxisNameID>258</AxisNameID>
+ </Axis>
+
+ <!-- Cd Hair -->
+ <!-- PostScript: NewFont-CdHair -->
+ <NamedInstance flags="0x0" postscriptNameID="260" subfamilyNameID="259">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Hair Italic -->
+ <!-- PostScript: NewFont-CdHairItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="262" subfamilyNameID="261">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Hair -->
+ <!-- PostScript: NewFont-Hair -->
+ <NamedInstance flags="0x0" postscriptNameID="264" subfamilyNameID="263">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Hair Italic -->
+ <!-- PostScript: NewFont-HairItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="266" subfamilyNameID="265">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex Hair -->
+ <!-- PostScript: NewFont-ExHair -->
+ <NamedInstance flags="0x0" postscriptNameID="268" subfamilyNameID="267">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Hair Italic -->
+ <!-- PostScript: NewFont-ExHairItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="270" subfamilyNameID="269">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd Thin -->
+ <!-- PostScript: NewFont-CdThin -->
+ <NamedInstance flags="0x0" postscriptNameID="272" subfamilyNameID="271">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Thin Italic -->
+ <!-- PostScript: NewFont-CdThinItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="274" subfamilyNameID="273">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Thin -->
+ <!-- PostScript: NewFont-Thin -->
+ <NamedInstance flags="0x0" postscriptNameID="276" subfamilyNameID="275">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Thin Italic -->
+ <!-- PostScript: NewFont-ThinItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="278" subfamilyNameID="277">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex Thin -->
+ <!-- PostScript: NewFont-ExThin -->
+ <NamedInstance flags="0x0" postscriptNameID="280" subfamilyNameID="279">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Thin Italic -->
+ <!-- PostScript: NewFont-ExThinItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="282" subfamilyNameID="281">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd Light -->
+ <!-- PostScript: NewFont-CdLight -->
+ <NamedInstance flags="0x0" postscriptNameID="284" subfamilyNameID="283">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Light Italic -->
+ <!-- PostScript: NewFont-CdLightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="286" subfamilyNameID="285">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Light -->
+ <!-- PostScript: NewFont-Light -->
+ <NamedInstance flags="0x0" postscriptNameID="288" subfamilyNameID="287">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Light Italic -->
+ <!-- PostScript: NewFont-LightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="290" subfamilyNameID="289">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex Light -->
+ <!-- PostScript: NewFont-ExLight -->
+ <NamedInstance flags="0x0" postscriptNameID="292" subfamilyNameID="291">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Light Italic -->
+ <!-- PostScript: NewFont-ExLightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="294" subfamilyNameID="293">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd -->
+ <!-- PostScript: NewFont-Cd -->
+ <NamedInstance flags="0x0" postscriptNameID="296" subfamilyNameID="295">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Italic -->
+ <!-- PostScript: NewFont-CdItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="298" subfamilyNameID="297">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+ <!-- PostScript: NewFont- -->
+ <NamedInstance flags="0x0" postscriptNameID="300" subfamilyNameID="299">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Italic -->
+ <!-- PostScript: NewFont-Italic -->
+ <NamedInstance flags="0x0" postscriptNameID="301" subfamilyNameID="258">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex -->
+ <!-- PostScript: NewFont-Ex -->
+ <NamedInstance flags="0x0" postscriptNameID="303" subfamilyNameID="302">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Italic -->
+ <!-- PostScript: NewFont-ExItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="305" subfamilyNameID="304">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd Medium -->
+ <!-- PostScript: NewFont-CdMedium -->
+ <NamedInstance flags="0x0" postscriptNameID="307" subfamilyNameID="306">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Medium Italic -->
+ <!-- PostScript: NewFont-CdMediumItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="309" subfamilyNameID="308">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Medium -->
+ <!-- PostScript: NewFont-Medium -->
+ <NamedInstance flags="0x0" postscriptNameID="311" subfamilyNameID="310">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Medium Italic -->
+ <!-- PostScript: NewFont-MediumItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="313" subfamilyNameID="312">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex Medium -->
+ <!-- PostScript: NewFont-ExMedium -->
+ <NamedInstance flags="0x0" postscriptNameID="315" subfamilyNameID="314">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Medium Italic -->
+ <!-- PostScript: NewFont-ExMediumItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="317" subfamilyNameID="316">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd SemiBold -->
+ <!-- PostScript: NewFont-CdSemiBold -->
+ <NamedInstance flags="0x0" postscriptNameID="319" subfamilyNameID="318">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd SemiBold Italic -->
+ <!-- PostScript: NewFont-CdSemiBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="321" subfamilyNameID="320">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- SemiBold -->
+ <!-- PostScript: NewFont-SemiBold -->
+ <NamedInstance flags="0x0" postscriptNameID="323" subfamilyNameID="322">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- SemiBold Italic -->
+ <!-- PostScript: NewFont-SemiBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="325" subfamilyNameID="324">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex SemiBold -->
+ <!-- PostScript: NewFont-ExSemiBold -->
+ <NamedInstance flags="0x0" postscriptNameID="327" subfamilyNameID="326">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex SemiBold Italic -->
+ <!-- PostScript: NewFont-ExSemiBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="329" subfamilyNameID="328">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd Bold -->
+ <!-- PostScript: NewFont-CdBold -->
+ <NamedInstance flags="0x0" postscriptNameID="331" subfamilyNameID="330">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Bold Italic -->
+ <!-- PostScript: NewFont-CdBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="333" subfamilyNameID="332">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Bold -->
+ <!-- PostScript: NewFont-Bold -->
+ <NamedInstance flags="0x0" postscriptNameID="335" subfamilyNameID="334">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Bold Italic -->
+ <!-- PostScript: NewFont-BoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="337" subfamilyNameID="336">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex Bold -->
+ <!-- PostScript: NewFont-ExBold -->
+ <NamedInstance flags="0x0" postscriptNameID="339" subfamilyNameID="338">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Bold Italic -->
+ <!-- PostScript: NewFont-ExBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="341" subfamilyNameID="340">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd XBold -->
+ <!-- PostScript: NewFont-CdXBold -->
+ <NamedInstance flags="0x0" postscriptNameID="343" subfamilyNameID="342">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd XBold Italic -->
+ <!-- PostScript: NewFont-CdXBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="345" subfamilyNameID="344">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- XBold -->
+ <!-- PostScript: NewFont-XBold -->
+ <NamedInstance flags="0x0" postscriptNameID="347" subfamilyNameID="346">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- XBold Italic -->
+ <!-- PostScript: NewFont-XBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="349" subfamilyNameID="348">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex XBold -->
+ <!-- PostScript: NewFont-ExXBold -->
+ <NamedInstance flags="0x0" postscriptNameID="351" subfamilyNameID="350">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex XBold Italic -->
+ <!-- PostScript: NewFont-ExXBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="353" subfamilyNameID="352">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Cd Black -->
+ <!-- PostScript: NewFont-CdBlack -->
+ <NamedInstance flags="0x0" postscriptNameID="355" subfamilyNameID="354">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Cd Black Italic -->
+ <!-- PostScript: NewFont-CdBlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="357" subfamilyNameID="356">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="75.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Black -->
+ <!-- PostScript: NewFont-Black -->
+ <NamedInstance flags="0x0" postscriptNameID="359" subfamilyNameID="358">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Black Italic -->
+ <!-- PostScript: NewFont-BlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="361" subfamilyNameID="360">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+
+ <!-- Ex Black -->
+ <!-- PostScript: NewFont-ExBlack -->
+ <NamedInstance flags="0x0" postscriptNameID="363" subfamilyNameID="362">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="0.0"/>
+ </NamedInstance>
+
+ <!-- Ex Black Italic -->
+ <!-- PostScript: NewFont-ExBlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="365" subfamilyNameID="364">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="125.0"/>
+ <coord axis="ital" value="1.0"/>
+ </NamedInstance>
+ </fvar>
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ </gvar>
+
+</ttFont>
diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py
index b9d4ffe9..db224cca 100644
--- a/Tests/varLib/instancer/instancer_test.py
+++ b/Tests/varLib/instancer/instancer_test.py
@@ -458,6 +458,8 @@ class InstantiateItemVariationStoreTest(object):
defaultDeltaArray = []
for varidx, delta in sorted(defaultDeltas.items()):
+ if varidx == varStore.NO_VARIATION_INDEX:
+ continue
major, minor = varidx >> 16, varidx & 0xFFFF
if major == len(defaultDeltaArray):
defaultDeltaArray.append([])
@@ -1975,3 +1977,35 @@ def test_main_exit_multiple_limits(varfont, tmpdir, capsys):
captured = capsys.readouterr()
assert "Specified multiple limits for the same axis" in captured.err
+
+
+def test_set_ribbi_bits():
+ varfont = ttLib.TTFont()
+ varfont.importXML(os.path.join(TESTDATA, "STATInstancerTest.ttx"))
+
+ for location in [instance.coordinates for instance in varfont["fvar"].instances]:
+ instance = instancer.instantiateVariableFont(
+ varfont, location, updateFontNames=True
+ )
+ name_id_2 = instance["name"].getDebugName(2)
+ mac_style = instance["head"].macStyle
+ fs_selection = instance["OS/2"].fsSelection & 0b1100001 # Just bits 0, 5, 6
+
+ if location["ital"] == 0:
+ if location["wght"] == 700:
+ assert name_id_2 == "Bold", location
+ assert mac_style == 0b01, location
+ assert fs_selection == 0b0100000, location
+ else:
+ assert name_id_2 == "Regular", location
+ assert mac_style == 0b00, location
+ assert fs_selection == 0b1000000, location
+ else:
+ if location["wght"] == 700:
+ assert name_id_2 == "Bold Italic", location
+ assert mac_style == 0b11, location
+ assert fs_selection == 0b0100001, location
+ else:
+ assert name_id_2 == "Italic", location
+ assert mac_style == 0b10, location
+ assert fs_selection == 0b0000001, location
diff --git a/Tests/varLib/iup_test.py b/Tests/varLib/iup_test.py
new file mode 100644
index 00000000..76b2af51
--- /dev/null
+++ b/Tests/varLib/iup_test.py
@@ -0,0 +1,53 @@
+import fontTools.varLib.iup as iup
+import sys
+import pytest
+
+
+class IupTest:
+
+# -----
+# Tests
+# -----
+
+ @pytest.mark.parametrize(
+ "delta, coords, forced",
+ [
+ (
+ [(0, 0)],
+ [(1, 2)],
+ set()
+ ),
+ (
+ [(0, 0), (0, 0), (0, 0)],
+ [(1, 2), (3, 2), (2, 3)],
+ set()
+ ),
+ (
+ [(1, 1), (-1, 1), (-1, -1), (1, -1)],
+ [(0, 0), (2, 0), (2, 2), (0, 2)],
+ set()
+ ),
+ (
+ [(-1, 0), (-1, 0), (-1, 0), (-1, 0), (-1, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (-1, 0)],
+ [(-35, -152), (-86, -101), (-50, -65), (0, -116), (51, -65), (86, -99), (35, -151), (87, -202), (51, -238), (-1, -187), (-53, -239), (-88, -205)],
+ {11}
+ ),
+ (
+ [(0, 0), (1, 0), (2, 0), (2, 0), (0, 0), (1, 0), (3, 0), (3, 0), (2, 0), (2, 0), (0, 0), (0, 0), (-1, 0), (-1, 0), (-1, 0), (-3, 0), (-1, 0), (0, 0), (0, 0), (-2, 0), (-2, 0), (-1, 0), (-1, 0), (-1, 0), (-4, 0)],
+ [(330, 65), (401, 65), (499, 117), (549, 225), (549, 308), (549, 422), (549, 500), (497, 600), (397, 648), (324, 648), (271, 648), (200, 620), (165, 570), (165, 536), (165, 473), (252, 407), (355, 407), (396, 407), (396, 333), (354, 333), (249, 333), (141, 268), (141, 203), (141, 131), (247, 65)],
+ {5, 15, 24}
+ ),
+ ]
+ )
+ def test_forced_set(self, delta, coords, forced):
+ f = iup._iup_contour_bound_forced_set(delta, coords)
+ assert forced == f
+
+ chain1, costs1 = iup._iup_contour_optimize_dp(delta, coords, f)
+ chain2, costs2 = iup._iup_contour_optimize_dp(delta, coords, set())
+
+ assert chain1 == chain2, f
+ assert costs1 == costs2, f
+
+if __name__ == "__main__":
+ sys.exit(pytest.main(sys.argv))
diff --git a/Tests/varLib/merger_test.py b/Tests/varLib/merger_test.py
new file mode 100644
index 00000000..aa7a6998
--- /dev/null
+++ b/Tests/varLib/merger_test.py
@@ -0,0 +1,1844 @@
+from copy import deepcopy
+import string
+from fontTools.colorLib.builder import LayerListBuilder, buildCOLR, buildClipList
+from fontTools.misc.testTools import getXML
+from fontTools.varLib.merger import COLRVariationMerger
+from fontTools.varLib.models import VariationModel
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.tables import otTables as ot
+from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter
+import pytest
+
+
+NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX
+
+
+def dump_xml(table, ttFont=None):
+ xml = getXML(table.toXML, ttFont)
+ print("[")
+ for line in xml:
+ print(f" {line!r},")
+ print("]")
+ return xml
+
+
+def compile_decompile(table, ttFont):
+ writer = OTTableWriter(tableTag="COLR")
+ # compile itself may modify a table, safer to copy it first
+ table = deepcopy(table)
+ table.compile(writer, ttFont)
+ data = writer.getAllData()
+
+ reader = OTTableReader(data, tableTag="COLR")
+ table2 = table.__class__()
+ table2.decompile(reader, ttFont)
+
+ return table2
+
+
+@pytest.fixture
+def ttFont():
+ font = TTFont()
+ font.setGlyphOrder([".notdef"] + list(string.ascii_letters))
+ return font
+
+
+def build_paint(data):
+ return LayerListBuilder().buildPaint(data)
+
+
+class COLRVariationMergerTest:
+ @pytest.mark.parametrize(
+ "paints, expected_xml, expected_varIdxes",
+ [
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ ],
+ [
+ '<Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ "</Paint>",
+ ],
+ [],
+ id="solid-same",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 0.5,
+ },
+ ],
+ [
+ '<Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ "</Paint>",
+ ],
+ [0],
+ id="solid-alpha",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintLinearGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "x1": 1,
+ "y1": 1,
+ "x2": 2,
+ "y2": 2,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintLinearGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.1, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 0.9, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "x1": 1,
+ "y1": 1,
+ "x2": 2,
+ "y2": 2,
+ },
+ ],
+ [
+ '<Paint Format="5"><!-- PaintVarLinearGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="2"/>',
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="0"/>',
+ ' <y0 value="0"/>',
+ ' <x1 value="1"/>',
+ ' <y1 value="1"/>',
+ ' <x2 value="2"/>',
+ ' <y2 value="2"/>',
+ " <VarIndexBase/>",
+ "</Paint>",
+ ],
+ [0, NO_VARIATION_INDEX, 1, NO_VARIATION_INDEX],
+ id="linear_grad-stop-offsets",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintLinearGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "x1": 1,
+ "y1": 1,
+ "x2": 2,
+ "y2": 2,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintLinearGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "x1": 1,
+ "y1": 1,
+ "x2": 2,
+ "y2": 2,
+ },
+ ],
+ [
+ '<Paint Format="5"><!-- PaintVarLinearGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " <VarIndexBase/>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="0"/>',
+ ' <y0 value="0"/>',
+ ' <x1 value="1"/>',
+ ' <y1 value="1"/>',
+ ' <x2 value="2"/>',
+ ' <y2 value="2"/>',
+ " <VarIndexBase/>",
+ "</Paint>",
+ ],
+ [NO_VARIATION_INDEX, 0],
+ id="linear_grad-stop[0].alpha",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintLinearGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "x1": 1,
+ "y1": 1,
+ "x2": 2,
+ "y2": 2,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintLinearGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": -0.5, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "x1": 1,
+ "y1": 1,
+ "x2": 2,
+ "y2": -200,
+ },
+ ],
+ [
+ '<Paint Format="5"><!-- PaintVarLinearGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " <VarIndexBase/>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="0"/>',
+ ' <y0 value="0"/>',
+ ' <x1 value="1"/>',
+ ' <y1 value="1"/>',
+ ' <x2 value="2"/>',
+ ' <y2 value="2"/>',
+ ' <VarIndexBase value="1"/>',
+ "</Paint>",
+ ],
+ [
+ 0,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ 1,
+ ],
+ id="linear_grad-stop[0].offset-y2",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintRadialGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "r0": 0,
+ "x1": 1,
+ "y1": 1,
+ "r1": 1,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintRadialGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.1, "PaletteIndex": 0, "Alpha": 0.6},
+ {"StopOffset": 0.9, "PaletteIndex": 1, "Alpha": 0.7},
+ ],
+ },
+ "x0": -1,
+ "y0": -2,
+ "r0": 3,
+ "x1": -4,
+ "y1": -5,
+ "r1": 6,
+ },
+ ],
+ [
+ '<Paint Format="7"><!-- PaintVarRadialGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="2"/>',
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="0"/>',
+ ' <y0 value="0"/>',
+ ' <r0 value="0"/>',
+ ' <x1 value="1"/>',
+ ' <y1 value="1"/>',
+ ' <r1 value="1"/>',
+ ' <VarIndexBase value="4"/>',
+ "</Paint>",
+ ],
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+ id="radial_grad-all-different",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintSweepGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.REPEAT),
+ "ColorStop": [
+ {"StopOffset": 0.4, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 0.6, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "centerX": 0,
+ "centerY": 0,
+ "startAngle": 0,
+ "endAngle": 180.0,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintSweepGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.REPEAT),
+ "ColorStop": [
+ {"StopOffset": 0.4, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 0.6, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "centerX": 0,
+ "centerY": 0,
+ "startAngle": 90.0,
+ "endAngle": 180.0,
+ },
+ ],
+ [
+ '<Paint Format="9"><!-- PaintVarSweepGradient -->',
+ " <ColorLine>",
+ ' <Extend value="repeat"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.4"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " <VarIndexBase/>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="0.6"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " <VarIndexBase/>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <centerX value="0"/>',
+ ' <centerY value="0"/>',
+ ' <startAngle value="0.0"/>',
+ ' <endAngle value="180.0"/>',
+ ' <VarIndexBase value="0"/>',
+ "</Paint>",
+ ],
+ [NO_VARIATION_INDEX, NO_VARIATION_INDEX, 0, NO_VARIATION_INDEX],
+ id="sweep_grad-startAngle",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintSweepGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 1.0},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 1.0},
+ ],
+ },
+ "centerX": 0,
+ "centerY": 0,
+ "startAngle": 0.0,
+ "endAngle": 180.0,
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintSweepGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0, "Alpha": 0.5},
+ {"StopOffset": 1.0, "PaletteIndex": 1, "Alpha": 0.5},
+ ],
+ },
+ "centerX": 0,
+ "centerY": 0,
+ "startAngle": 0.0,
+ "endAngle": 180.0,
+ },
+ ],
+ [
+ '<Paint Format="9"><!-- PaintVarSweepGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <centerX value="0"/>',
+ ' <centerY value="0"/>',
+ ' <startAngle value="0.0"/>',
+ ' <endAngle value="180.0"/>',
+ " <VarIndexBase/>",
+ "</Paint>",
+ ],
+ [NO_VARIATION_INDEX, 0],
+ id="sweep_grad-stops-alpha-reuse-varidxbase",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": int(ot.PaintFormat.PaintTransform),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintRadialGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {
+ "StopOffset": 0.0,
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ {
+ "StopOffset": 1.0,
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "r0": 0,
+ "x1": 1,
+ "y1": 1,
+ "r1": 1,
+ },
+ "Transform": {
+ "xx": 1.0,
+ "xy": 0.0,
+ "yx": 0.0,
+ "yy": 1.0,
+ "dx": 0.0,
+ "dy": 0.0,
+ },
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintTransform),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintRadialGradient),
+ "ColorLine": {
+ "Extend": int(ot.ExtendMode.PAD),
+ "ColorStop": [
+ {
+ "StopOffset": 0.0,
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ {
+ "StopOffset": 1.0,
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ ],
+ },
+ "x0": 0,
+ "y0": 0,
+ "r0": 0,
+ "x1": 1,
+ "y1": 1,
+ "r1": 1,
+ },
+ "Transform": {
+ "xx": 1.0,
+ "xy": 0.0,
+ "yx": 0.0,
+ "yy": 0.5,
+ "dx": 0.0,
+ "dy": -100.0,
+ },
+ },
+ ],
+ [
+ '<Paint Format="13"><!-- PaintVarTransform -->',
+ ' <Paint Format="6"><!-- PaintRadialGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <x0 value="0"/>',
+ ' <y0 value="0"/>',
+ ' <r0 value="0"/>',
+ ' <x1 value="1"/>',
+ ' <y1 value="1"/>',
+ ' <r1 value="1"/>',
+ " </Paint>",
+ " <Transform>",
+ ' <xx value="1.0"/>',
+ ' <yx value="0.0"/>',
+ ' <xy value="0.0"/>',
+ ' <yy value="1.0"/>',
+ ' <dx value="0.0"/>',
+ ' <dy value="0.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Transform>",
+ "</Paint>",
+ ],
+ [
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ 0,
+ NO_VARIATION_INDEX,
+ 1,
+ ],
+ id="transform-yy-dy",
+ ),
+ pytest.param(
+ [
+ {
+ "Format": ot.PaintFormat.PaintTransform,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintSweepGradient,
+ "ColorLine": {
+ "Extend": ot.ExtendMode.PAD,
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0},
+ {
+ "StopOffset": 1.0,
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ ],
+ },
+ "centerX": 0,
+ "centerY": 0,
+ "startAngle": 0,
+ "endAngle": 360,
+ },
+ "Transform": (1.0, 0, 0, 1.0, 0, 0),
+ },
+ {
+ "Format": ot.PaintFormat.PaintTransform,
+ "Paint": {
+ "Format": ot.PaintFormat.PaintSweepGradient,
+ "ColorLine": {
+ "Extend": ot.ExtendMode.PAD,
+ "ColorStop": [
+ {"StopOffset": 0.0, "PaletteIndex": 0},
+ {
+ "StopOffset": 1.0,
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ ],
+ },
+ "centerX": 256,
+ "centerY": 0,
+ "startAngle": 0,
+ "endAngle": 360,
+ },
+ # Transform.xx below produces the same VarStore delta as the
+ # above PaintSweepGradient's centerX because, when Fixed16.16
+ # is converted to integer, it becomes:
+ # floatToFixed(1.00390625, 16) == 256
+ # Because there is overlap between the varIdxes of the
+ # PaintVarTransform's Affine2x3 and the PaintSweepGradient's
+ # the VarIndexBase is reused (0 for both)
+ "Transform": (1.00390625, 0, 0, 1.0, 10, 0),
+ },
+ ],
+ [
+ '<Paint Format="13"><!-- PaintVarTransform -->',
+ ' <Paint Format="9"><!-- PaintVarSweepGradient -->',
+ " <ColorLine>",
+ ' <Extend value="pad"/>',
+ " <!-- StopCount=2 -->",
+ ' <ColorStop index="0">',
+ ' <StopOffset value="0.0"/>',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " <VarIndexBase/>",
+ " </ColorStop>",
+ ' <ColorStop index="1">',
+ ' <StopOffset value="1.0"/>',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " <VarIndexBase/>",
+ " </ColorStop>",
+ " </ColorLine>",
+ ' <centerX value="0"/>',
+ ' <centerY value="0"/>',
+ ' <startAngle value="0.0"/>',
+ ' <endAngle value="360.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Paint>",
+ " <Transform>",
+ ' <xx value="1.0"/>',
+ ' <yx value="0.0"/>',
+ ' <xy value="0.0"/>',
+ ' <yy value="1.0"/>',
+ ' <dx value="0.0"/>',
+ ' <dy value="0.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Transform>",
+ "</Paint>",
+ ],
+ [
+ 0,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ 1,
+ NO_VARIATION_INDEX,
+ ],
+ id="transform-xx-sweep_grad-centerx-same-varidxbase",
+ ),
+ ],
+ )
+ def test_merge_Paint(self, paints, ttFont, expected_xml, expected_varIdxes):
+ paints = [build_paint(p) for p in paints]
+ out = deepcopy(paints[0])
+
+ model = VariationModel([{}, {"ZZZZ": 1.0}])
+ merger = COLRVariationMerger(model, ["ZZZZ"], ttFont)
+
+ merger.mergeThings(out, paints)
+
+ assert compile_decompile(out, ttFont) == out
+ assert dump_xml(out, ttFont) == expected_xml
+ assert merger.varIdxes == expected_varIdxes
+
+ def test_merge_ClipList(self, ttFont):
+ clipLists = [
+ buildClipList(clips)
+ for clips in [
+ {
+ "A": (0, 0, 1000, 1000),
+ "B": (0, 0, 1000, 1000),
+ "C": (0, 0, 1000, 1000),
+ "D": (0, 0, 1000, 1000),
+ },
+ {
+ # non-default masters' clip boxes can be 'sparse'
+ # (i.e. can omit explicit clip box for some glyphs)
+ # "A": (0, 0, 1000, 1000),
+ "B": (10, 0, 1000, 1000),
+ "C": (20, 20, 1020, 1020),
+ "D": (20, 20, 1020, 1020),
+ },
+ ]
+ ]
+ out = deepcopy(clipLists[0])
+
+ model = VariationModel([{}, {"ZZZZ": 1.0}])
+ merger = COLRVariationMerger(model, ["ZZZZ"], ttFont)
+
+ merger.mergeThings(out, clipLists)
+
+ assert compile_decompile(out, ttFont) == out
+ assert dump_xml(out, ttFont) == [
+ '<ClipList Format="1">',
+ " <Clip>",
+ ' <Glyph value="A"/>',
+ ' <ClipBox Format="1">',
+ ' <xMin value="0"/>',
+ ' <yMin value="0"/>',
+ ' <xMax value="1000"/>',
+ ' <yMax value="1000"/>',
+ " </ClipBox>",
+ " </Clip>",
+ " <Clip>",
+ ' <Glyph value="B"/>',
+ ' <ClipBox Format="2">',
+ ' <xMin value="0"/>',
+ ' <yMin value="0"/>',
+ ' <xMax value="1000"/>',
+ ' <yMax value="1000"/>',
+ ' <VarIndexBase value="0"/>',
+ " </ClipBox>",
+ " </Clip>",
+ " <Clip>",
+ ' <Glyph value="C"/>',
+ ' <Glyph value="D"/>',
+ ' <ClipBox Format="2">',
+ ' <xMin value="0"/>',
+ ' <yMin value="0"/>',
+ ' <xMax value="1000"/>',
+ ' <yMax value="1000"/>',
+ ' <VarIndexBase value="4"/>',
+ " </ClipBox>",
+ " </Clip>",
+ "</ClipList>",
+ ]
+ assert merger.varIdxes == [
+ 0,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ NO_VARIATION_INDEX,
+ 1,
+ 1,
+ 1,
+ 1,
+ ]
+
+ @pytest.mark.parametrize(
+ "master_layer_reuse",
+ [
+ pytest.param(False, id="no-reuse"),
+ pytest.param(True, id="with-reuse"),
+ ],
+ )
+ @pytest.mark.parametrize(
+ "color_glyphs, output_layer_reuse, expected_xml, expected_varIdxes",
+ [
+ pytest.param(
+ [
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ {
+ "A": {
+ "Format": ot.PaintFormat.PaintColrLayers,
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ ],
+ False,
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=1 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ " <LayerList>",
+ " <!-- LayerCount=2 -->",
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ " </LayerList>",
+ "</COLR>",
+ ],
+ [],
+ id="no-variation",
+ ),
+ pytest.param(
+ [
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "C": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 3,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ {
+ # NOTE: 'A' is missing from non-default master
+ "C": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 3,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ ],
+ False,
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=2 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="1">',
+ ' <BaseGlyph value="C"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="2"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ " <LayerList>",
+ " <!-- LayerCount=4 -->",
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="2" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="3" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="3"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ " </LayerList>",
+ "</COLR>",
+ ],
+ [0],
+ id="sparse-masters",
+ ),
+ pytest.param(
+ [
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "C": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ # 'C' reuses layers 1-3 from 'A'
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "D": { # identical to 'C'
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "E": { # superset of 'C' or 'D'
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 3,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ {
+ # NOTE: 'A' is missing from non-default master
+ "C": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "D": { # same as 'C'
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "E": { # first two layers vary the same way as 'C' or 'D'
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 3,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ ],
+ True, # reuse
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=4 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="3"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="1">',
+ ' <BaseGlyph value="C"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="3"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="2">',
+ ' <BaseGlyph value="D"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="3"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="3">',
+ ' <BaseGlyph value="E"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="5"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ " <LayerList>",
+ " <!-- LayerCount=7 -->",
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="2" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="3" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="4" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="5" Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="3"/>',
+ " </Paint>",
+ ' <Paint index="6" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="3"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ " </LayerList>",
+ "</COLR>",
+ ],
+ [0],
+ id="sparse-masters-with-reuse",
+ ),
+ pytest.param(
+ [
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "C": { # 'C' shares layer 1 and 2 with 'A'
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 0.9,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ "C": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 0.5,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ ],
+ },
+ },
+ ],
+ True,
+ [
+ # a different Alpha variation is applied to a shared layer between
+ # 'A' and 'C' and thus they are no longer shared.
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=2 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="3"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="1">',
+ ' <BaseGlyph value="C"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="3"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ " <LayerList>",
+ " <!-- LayerCount=5 -->",
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="2" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="3" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="3"><!-- PaintVarSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ ' <VarIndexBase value="1"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="4" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ " </LayerList>",
+ "</COLR>",
+ ],
+ [0, 1],
+ id="shared-master-layers-different-variations",
+ ),
+ ],
+ )
+ def test_merge_full_table(
+ self,
+ color_glyphs,
+ ttFont,
+ expected_xml,
+ expected_varIdxes,
+ master_layer_reuse,
+ output_layer_reuse,
+ ):
+ master_ttfs = [deepcopy(ttFont) for _ in range(len(color_glyphs))]
+ for ttf, glyphs in zip(master_ttfs, color_glyphs):
+ # merge algorithm is expected to work the same even if the master COLRs
+ # may differ as to the layer reuse, hence we try both ways
+ ttf["COLR"] = buildCOLR(glyphs, allowLayerReuse=master_layer_reuse)
+ vf = deepcopy(master_ttfs[0])
+
+ model = VariationModel([{}, {"ZZZZ": 1.0}])
+ merger = COLRVariationMerger(
+ model, ["ZZZZ"], vf, allowLayerReuse=output_layer_reuse
+ )
+
+ merger.mergeTables(vf, master_ttfs)
+
+ out = vf["COLR"].table
+
+ assert compile_decompile(out, vf) == out
+ assert dump_xml(out, vf) == expected_xml
+ assert merger.varIdxes == expected_varIdxes
+
+ @pytest.mark.parametrize(
+ "color_glyphs, before_xml, expected_xml",
+ [
+ pytest.param(
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "C",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "D",
+ },
+ ],
+ },
+ "E": {
+ "Format": int(ot.PaintFormat.PaintColrLayers),
+ "Layers": [
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 1,
+ "Alpha": 1.0,
+ },
+ "Glyph": "C",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 2,
+ "Alpha": 1.0,
+ },
+ "Glyph": "D",
+ },
+ {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 3,
+ "Alpha": 1.0,
+ },
+ "Glyph": "F",
+ },
+ ],
+ },
+ "G": {
+ "Format": int(ot.PaintFormat.PaintColrGlyph),
+ "Glyph": "E",
+ },
+ },
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=3 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="3"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="1">',
+ ' <BaseGlyph value="E"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="3"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="2">',
+ ' <BaseGlyph value="G"/>',
+ ' <Paint Format="11"><!-- PaintColrGlyph -->',
+ ' <Glyph value="E"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ " <LayerList>",
+ " <!-- LayerCount=5 -->",
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="C"/>',
+ " </Paint>",
+ ' <Paint index="2" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="D"/>',
+ " </Paint>",
+ ' <Paint index="3" Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="2"/>',
+ ' <FirstLayerIndex value="1"/>',
+ " </Paint>",
+ ' <Paint index="4" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="3"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="F"/>',
+ " </Paint>",
+ " </LayerList>",
+ "</COLR>",
+ ],
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=3 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="3"/>',
+ ' <FirstLayerIndex value="0"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="1">',
+ ' <BaseGlyph value="E"/>',
+ ' <Paint Format="1"><!-- PaintColrLayers -->',
+ ' <NumLayers value="3"/>',
+ ' <FirstLayerIndex value="3"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ ' <BaseGlyphPaintRecord index="2">',
+ ' <BaseGlyph value="G"/>',
+ ' <Paint Format="11"><!-- PaintColrGlyph -->',
+ ' <Glyph value="E"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ " <LayerList>",
+ " <!-- LayerCount=6 -->",
+ ' <Paint index="0" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ ' <Paint index="1" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="C"/>',
+ " </Paint>",
+ ' <Paint index="2" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="D"/>',
+ " </Paint>",
+ ' <Paint index="3" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="1"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="C"/>',
+ " </Paint>",
+ ' <Paint index="4" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="2"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="D"/>',
+ " </Paint>",
+ ' <Paint index="5" Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="3"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="F"/>',
+ " </Paint>",
+ " </LayerList>",
+ "</COLR>",
+ ],
+ id="simple-reuse",
+ ),
+ pytest.param(
+ {
+ "A": {
+ "Format": int(ot.PaintFormat.PaintGlyph),
+ "Paint": {
+ "Format": int(ot.PaintFormat.PaintSolid),
+ "PaletteIndex": 0,
+ "Alpha": 1.0,
+ },
+ "Glyph": "B",
+ },
+ },
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=1 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ "</COLR>",
+ ],
+ [
+ "<COLR>",
+ ' <Version value="1"/>',
+ " <!-- BaseGlyphRecordCount=0 -->",
+ " <!-- LayerRecordCount=0 -->",
+ " <BaseGlyphList>",
+ " <!-- BaseGlyphCount=1 -->",
+ ' <BaseGlyphPaintRecord index="0">',
+ ' <BaseGlyph value="A"/>',
+ ' <Paint Format="10"><!-- PaintGlyph -->',
+ ' <Paint Format="2"><!-- PaintSolid -->',
+ ' <PaletteIndex value="0"/>',
+ ' <Alpha value="1.0"/>',
+ " </Paint>",
+ ' <Glyph value="B"/>',
+ " </Paint>",
+ " </BaseGlyphPaintRecord>",
+ " </BaseGlyphList>",
+ "</COLR>",
+ ],
+ id="no-layer-list",
+ ),
+ ],
+ )
+ def test_expandPaintColrLayers(
+ self, color_glyphs, ttFont, before_xml, expected_xml
+ ):
+ colr = buildCOLR(color_glyphs, allowLayerReuse=True)
+
+ assert dump_xml(colr.table, ttFont) == before_xml
+
+ before_layer_count = 0
+ reuses_colr_layers = False
+ if colr.table.LayerList:
+ before_layer_count = len(colr.table.LayerList.Paint)
+ reuses_colr_layers = any(
+ p.Format == ot.PaintFormat.PaintColrLayers
+ for p in colr.table.LayerList.Paint
+ )
+
+ COLRVariationMerger.expandPaintColrLayers(colr.table)
+
+ assert dump_xml(colr.table, ttFont) == expected_xml
+
+ after_layer_count = (
+ 0 if not colr.table.LayerList else len(colr.table.LayerList.Paint)
+ )
+
+ if reuses_colr_layers:
+ assert not any(
+ p.Format == ot.PaintFormat.PaintColrLayers
+ for p in colr.table.LayerList.Paint
+ )
+ assert after_layer_count > before_layer_count
+ else:
+ assert after_layer_count == before_layer_count
+
+ if colr.table.LayerList:
+ assert len({id(p) for p in colr.table.LayerList.Paint}) == after_layer_count
diff --git a/Tests/varLib/models_test.py b/Tests/varLib/models_test.py
index c220d3d2..e0080129 100644
--- a/Tests/varLib/models_test.py
+++ b/Tests/varLib/models_test.py
@@ -1,78 +1,120 @@
from fontTools.varLib.models import (
- normalizeLocation, supportScalar, VariationModel, VariationModelError)
+ normalizeLocation,
+ supportScalar,
+ VariationModel,
+ VariationModelError,
+)
import pytest
def test_normalizeLocation():
axes = {"wght": (100, 400, 900)}
- assert normalizeLocation({"wght": 400}, axes) == {'wght': 0.0}
- assert normalizeLocation({"wght": 100}, axes) == {'wght': -1.0}
- assert normalizeLocation({"wght": 900}, axes) == {'wght': 1.0}
- assert normalizeLocation({"wght": 650}, axes) == {'wght': 0.5}
- assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
- assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}
+ assert normalizeLocation({"wght": 400}, axes) == {"wght": 0.0}
+ assert normalizeLocation({"wght": 100}, axes) == {"wght": -1.0}
+ assert normalizeLocation({"wght": 900}, axes) == {"wght": 1.0}
+ assert normalizeLocation({"wght": 650}, axes) == {"wght": 0.5}
+ assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0}
+ assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0}
axes = {"wght": (0, 0, 1000)}
- assert normalizeLocation({"wght": 0}, axes) == {'wght': 0.0}
- assert normalizeLocation({"wght": -1}, axes) == {'wght': 0.0}
- assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0}
- assert normalizeLocation({"wght": 500}, axes) == {'wght': 0.5}
- assert normalizeLocation({"wght": 1001}, axes) == {'wght': 1.0}
+ assert normalizeLocation({"wght": 0}, axes) == {"wght": 0.0}
+ assert normalizeLocation({"wght": -1}, axes) == {"wght": 0.0}
+ assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0}
+ assert normalizeLocation({"wght": 500}, axes) == {"wght": 0.5}
+ assert normalizeLocation({"wght": 1001}, axes) == {"wght": 1.0}
axes = {"wght": (0, 1000, 1000)}
- assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0}
- assert normalizeLocation({"wght": -1}, axes) == {'wght': -1.0}
- assert normalizeLocation({"wght": 500}, axes) == {'wght': -0.5}
- assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0.0}
- assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0.0}
+ assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0}
+ assert normalizeLocation({"wght": -1}, axes) == {"wght": -1.0}
+ assert normalizeLocation({"wght": 500}, axes) == {"wght": -0.5}
+ assert normalizeLocation({"wght": 1000}, axes) == {"wght": 0.0}
+ assert normalizeLocation({"wght": 1001}, axes) == {"wght": 0.0}
def test_supportScalar():
assert supportScalar({}, {}) == 1.0
- assert supportScalar({'wght':.2}, {}) == 1.0
- assert supportScalar({'wght':.2}, {'wght':(0,2,3)}) == 0.1
- assert supportScalar({'wght':2.5}, {'wght':(0,2,4)}) == 0.75
+ assert supportScalar({"wght": 0.2}, {}) == 1.0
+ assert supportScalar({"wght": 0.2}, {"wght": (0, 2, 3)}) == 0.1
+ assert supportScalar({"wght": 2.5}, {"wght": (0, 2, 4)}) == 0.75
+ assert supportScalar({"wght": 4}, {"wght": (0, 2, 2)}) == 0.0
+ assert supportScalar({"wght": 4}, {"wght": (0, 2, 2)}, extrapolate=True) == 2.0
+ assert supportScalar({"wght": 4}, {"wght": (0, 2, 3)}, extrapolate=True) == 2.0
+ assert supportScalar({"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True) == -4.0
-class VariationModelTest(object):
+@pytest.mark.parametrize(
+ "numLocations, numSamples",
+ [
+ pytest.param(127, 509, marks=pytest.mark.slow),
+ (31, 251),
+ ],
+)
+def test_modeling_error(numLocations, numSamples):
+ # https://github.com/fonttools/fonttools/issues/2213
+ locations = [{"axis": float(i) / numLocations} for i in range(numLocations)]
+ masterValues = [100.0 if i else 0.0 for i in range(numLocations)]
+
+ model = VariationModel(locations)
+
+ for i in range(numSamples):
+ loc = {"axis": float(i) / numSamples}
+ scalars = model.getScalars(loc)
+
+ deltas_float = model.getDeltas(masterValues)
+ deltas_round = model.getDeltas(masterValues, round=round)
+
+ expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars)
+ actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars)
+ err = abs(actual - expected)
+ assert err <= 0.5, (i, err)
+
+ # This is how NOT to round deltas.
+ # deltas_late_round = [round(d) for d in deltas_float]
+ # bad = model.interpolateFromDeltasAndScalars(deltas_late_round, scalars)
+ # err_bad = abs(bad - expected)
+ # if err != err_bad:
+ # print("{:d} {:.2} {:.2}".format(i, err, err_bad))
+
+
+class VariationModelTest(object):
@pytest.mark.parametrize(
"locations, axisOrder, sortedLocs, supports, deltaWeights",
[
(
[
- {'wght': 0.55, 'wdth': 0.0},
- {'wght': -0.55, 'wdth': 0.0},
- {'wght': -1.0, 'wdth': 0.0},
- {'wght': 0.0, 'wdth': 1.0},
- {'wght': 0.66, 'wdth': 1.0},
- {'wght': 0.66, 'wdth': 0.66},
- {'wght': 0.0, 'wdth': 0.0},
- {'wght': 1.0, 'wdth': 1.0},
- {'wght': 1.0, 'wdth': 0.0},
+ {"wght": 0.55, "wdth": 0.0},
+ {"wght": -0.55, "wdth": 0.0},
+ {"wght": -1.0, "wdth": 0.0},
+ {"wght": 0.0, "wdth": 1.0},
+ {"wght": 0.66, "wdth": 1.0},
+ {"wght": 0.66, "wdth": 0.66},
+ {"wght": 0.0, "wdth": 0.0},
+ {"wght": 1.0, "wdth": 1.0},
+ {"wght": 1.0, "wdth": 0.0},
],
["wght"],
[
{},
- {'wght': -0.55},
- {'wght': -1.0},
- {'wght': 0.55},
- {'wght': 1.0},
- {'wdth': 1.0},
- {'wdth': 1.0, 'wght': 1.0},
- {'wdth': 1.0, 'wght': 0.66},
- {'wdth': 0.66, 'wght': 0.66}
+ {"wght": -0.55},
+ {"wght": -1.0},
+ {"wght": 0.55},
+ {"wght": 1.0},
+ {"wdth": 1.0},
+ {"wdth": 1.0, "wght": 1.0},
+ {"wdth": 1.0, "wght": 0.66},
+ {"wdth": 0.66, "wght": 0.66},
],
[
{},
- {'wght': (-1.0, -0.55, 0)},
- {'wght': (-1.0, -1.0, -0.55)},
- {'wght': (0, 0.55, 1.0)},
- {'wght': (0.55, 1.0, 1.0)},
- {'wdth': (0, 1.0, 1.0)},
- {'wdth': (0, 1.0, 1.0), 'wght': (0, 1.0, 1.0)},
- {'wdth': (0, 1.0, 1.0), 'wght': (0, 0.66, 1.0)},
- {'wdth': (0, 0.66, 1.0), 'wght': (0, 0.66, 1.0)}
+ {"wght": (-1.0, -0.55, 0)},
+ {"wght": (-1.0, -1.0, -0.55)},
+ {"wght": (0, 0.55, 1.0)},
+ {"wght": (0.55, 1.0, 1.0)},
+ {"wdth": (0, 1.0, 1.0)},
+ {"wdth": (0, 1.0, 1.0), "wght": (0, 1.0, 1.0)},
+ {"wdth": (0, 1.0, 1.0), "wght": (0, 0.66, 1.0)},
+ {"wdth": (0, 0.66, 1.0), "wght": (0, 0.66, 1.0)},
],
[
{},
@@ -81,47 +123,49 @@ class VariationModelTest(object):
{0: 1.0},
{0: 1.0},
{0: 1.0},
- {0: 1.0,
- 4: 1.0,
- 5: 1.0},
- {0: 1.0,
- 3: 0.7555555555555555,
- 4: 0.24444444444444444,
- 5: 1.0,
- 6: 0.66},
- {0: 1.0,
- 3: 0.7555555555555555,
- 4: 0.24444444444444444,
- 5: 0.66,
- 6: 0.43560000000000006,
- 7: 0.66}
- ]
+ {0: 1.0, 4: 1.0, 5: 1.0},
+ {
+ 0: 1.0,
+ 3: 0.7555555555555555,
+ 4: 0.24444444444444444,
+ 5: 1.0,
+ 6: 0.66,
+ },
+ {
+ 0: 1.0,
+ 3: 0.7555555555555555,
+ 4: 0.24444444444444444,
+ 5: 0.66,
+ 6: 0.43560000000000004,
+ 7: 0.66,
+ },
+ ],
),
(
[
{},
- {'bar': 0.5},
- {'bar': 1.0},
- {'foo': 1.0},
- {'bar': 0.5, 'foo': 1.0},
- {'bar': 1.0, 'foo': 1.0},
+ {"bar": 0.5},
+ {"bar": 1.0},
+ {"foo": 1.0},
+ {"bar": 0.5, "foo": 1.0},
+ {"bar": 1.0, "foo": 1.0},
],
None,
[
{},
- {'bar': 0.5},
- {'bar': 1.0},
- {'foo': 1.0},
- {'bar': 0.5, 'foo': 1.0},
- {'bar': 1.0, 'foo': 1.0},
+ {"bar": 0.5},
+ {"bar": 1.0},
+ {"foo": 1.0},
+ {"bar": 0.5, "foo": 1.0},
+ {"bar": 1.0, "foo": 1.0},
],
[
{},
- {'bar': (0, 0.5, 1.0)},
- {'bar': (0.5, 1.0, 1.0)},
- {'foo': (0, 1.0, 1.0)},
- {'bar': (0, 0.5, 1.0), 'foo': (0, 1.0, 1.0)},
- {'bar': (0.5, 1.0, 1.0), 'foo': (0, 1.0, 1.0)},
+ {"bar": (0, 0.5, 1.0)},
+ {"bar": (0.5, 1.0, 1.0)},
+ {"foo": (0, 1.0, 1.0)},
+ {"bar": (0, 0.5, 1.0), "foo": (0, 1.0, 1.0)},
+ {"bar": (0.5, 1.0, 1.0), "foo": (0, 1.0, 1.0)},
],
[
{},
@@ -131,12 +175,96 @@ class VariationModelTest(object):
{0: 1.0, 1: 1.0, 3: 1.0},
{0: 1.0, 2: 1.0, 3: 1.0},
],
- )
- ]
+ ),
+ (
+ [
+ {},
+ {"foo": 0.25},
+ {"foo": 0.5},
+ {"foo": 0.75},
+ {"foo": 1.0},
+ {"bar": 0.25},
+ {"bar": 0.75},
+ {"bar": 1.0},
+ ],
+ None,
+ [
+ {},
+ {"bar": 0.25},
+ {"bar": 0.75},
+ {"bar": 1.0},
+ {"foo": 0.25},
+ {"foo": 0.5},
+ {"foo": 0.75},
+ {"foo": 1.0},
+ ],
+ [
+ {},
+ {"bar": (0.0, 0.25, 1.0)},
+ {"bar": (0.25, 0.75, 1.0)},
+ {"bar": (0.75, 1.0, 1.0)},
+ {"foo": (0.0, 0.25, 1.0)},
+ {"foo": (0.25, 0.5, 1.0)},
+ {"foo": (0.5, 0.75, 1.0)},
+ {"foo": (0.75, 1.0, 1.0)},
+ ],
+ [
+ {},
+ {0: 1.0},
+ {0: 1.0, 1: 0.3333333333333333},
+ {0: 1.0},
+ {0: 1.0},
+ {0: 1.0, 4: 0.6666666666666666},
+ {0: 1.0, 4: 0.3333333333333333, 5: 0.5},
+ {0: 1.0},
+ ],
+ ),
+ (
+ [
+ {},
+ {"foo": 0.25},
+ {"foo": 0.5},
+ {"foo": 0.75},
+ {"foo": 1.0},
+ {"bar": 0.25},
+ {"bar": 0.75},
+ {"bar": 1.0},
+ ],
+ None,
+ [
+ {},
+ {"bar": 0.25},
+ {"bar": 0.75},
+ {"bar": 1.0},
+ {"foo": 0.25},
+ {"foo": 0.5},
+ {"foo": 0.75},
+ {"foo": 1.0},
+ ],
+ [
+ {},
+ {"bar": (0, 0.25, 1.0)},
+ {"bar": (0.25, 0.75, 1.0)},
+ {"bar": (0.75, 1.0, 1.0)},
+ {"foo": (0, 0.25, 1.0)},
+ {"foo": (0.25, 0.5, 1.0)},
+ {"foo": (0.5, 0.75, 1.0)},
+ {"foo": (0.75, 1.0, 1.0)},
+ ],
+ [
+ {},
+ {0: 1.0},
+ {0: 1.0, 1: 0.3333333333333333},
+ {0: 1.0},
+ {0: 1.0},
+ {0: 1.0, 4: 0.6666666666666666},
+ {0: 1.0, 4: 0.3333333333333333, 5: 0.5},
+ {0: 1.0},
+ ],
+ ),
+ ],
)
- def test_init(
- self, locations, axisOrder, sortedLocs, supports, deltaWeights
- ):
+ def test_init(self, locations, axisOrder, sortedLocs, supports, deltaWeights):
model = VariationModel(locations, axisOrder=axisOrder)
assert model.locations == sortedLocs
@@ -152,3 +280,45 @@ class VariationModelTest(object):
{"bar": 1.0, "foo": 1.0},
]
)
+
+ @pytest.mark.parametrize(
+ "locations, axisOrder, masterValues, instanceLocation, expectedValue",
+ [
+ (
+ [
+ {},
+ {"axis_A": 1.0},
+ {"axis_B": 1.0},
+ {"axis_A": 1.0, "axis_B": 1.0},
+ {"axis_A": 0.5, "axis_B": 1.0},
+ {"axis_A": 1.0, "axis_B": 0.5},
+ ],
+ ["axis_A", "axis_B"],
+ [
+ 0,
+ 10,
+ 20,
+ 70,
+ 50,
+ 60,
+ ],
+ {
+ "axis_A": 0.5,
+ "axis_B": 0.5,
+ },
+ 37.5,
+ ),
+ ],
+ )
+ def test_interpolation(
+ self,
+ locations,
+ axisOrder,
+ masterValues,
+ instanceLocation,
+ expectedValue,
+ ):
+ model = VariationModel(locations, axisOrder=axisOrder)
+ interpolatedValue = model.interpolateFromMasters(instanceLocation, masterValues)
+
+ assert interpolatedValue == expectedValue
diff --git a/Tests/varLib/stat_test.py b/Tests/varLib/stat_test.py
index 6aa88a05..6def990e 100644
--- a/Tests/varLib/stat_test.py
+++ b/Tests/varLib/stat_test.py
@@ -15,7 +15,7 @@ def test_getStatAxes(datadir):
doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
assert getStatAxes(
- doc, {"Italic": 0, "width": Range(50, 150), "weight": Range(200, 900)}
+ doc, {"Italic": 0, "Width": Range(50, 150), "Weight": Range(200, 900)}
) == [
{
"values": [
@@ -82,7 +82,7 @@ def test_getStatAxes(datadir):
"rangeMinValue": 150.0,
},
],
- "name": {"en": "width", "fr": "Chasse"},
+ "name": {"en": "Width", "fr": "Chasse"},
"ordering": 1,
"tag": "wdth",
},
@@ -96,7 +96,7 @@ def test_getStatAxes(datadir):
},
]
- assert getStatAxes(doc, {"Italic": 1, "width": 100, "weight": Range(400, 700)}) == [
+ assert getStatAxes(doc, {"Italic": 1, "Width": 100, "Weight": Range(400, 700)}) == [
{
"values": [
{
@@ -129,7 +129,7 @@ def test_getStatAxes(datadir):
"values": [
{"flags": 3, "name": {"en": "Normal"}, "value": 100.0},
],
- "name": {"en": "width", "fr": "Chasse"},
+ "name": {"en": "Width", "fr": "Chasse"},
"ordering": 1,
"tag": "wdth",
},
@@ -148,7 +148,7 @@ def test_getStatLocations(datadir):
doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
assert getStatLocations(
- doc, {"Italic": 0, "width": Range(50, 150), "weight": Range(200, 900)}
+ doc, {"Italic": 0, "Width": Range(50, 150), "Weight": Range(200, 900)}
) == [
{
"flags": 0,
@@ -157,7 +157,7 @@ def test_getStatLocations(datadir):
},
]
assert getStatLocations(
- doc, {"Italic": 1, "width": Range(50, 150), "weight": Range(200, 900)}
+ doc, {"Italic": 1, "Width": Range(50, 150), "Weight": Range(200, 900)}
) == [
{
"flags": 0,
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
index 484a2e22..29f909ae 100644
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -823,6 +823,7 @@ been the same. This happened while performing the following operation:
GPOS.table.FeatureList.FeatureCount
The problem is likely to be in Simple Two Axis Bold:
+Expected to see .FeatureCount==2, instead saw 1
Incompatible features between masters.
Expected: kern, mark.
@@ -840,7 +841,7 @@ Got: kern.
def test_varlib_build_incompatible_lookup_types(self):
with pytest.raises(
varLibErrors.MismatchedTypes,
- match = r"MarkBasePos, instead saw PairPos"
+ match = r"'MarkBasePos', instead saw 'PairPos'"
):
self._run_varlib_build_test(
designspace_name="IncompatibleLookupTypes",
@@ -870,6 +871,15 @@ Expected to see .ScriptCount==1, instead saw 0"""
save_before_dump=True,
)
+ def test_varlib_build_variable_colr(self):
+ self._run_varlib_build_test(
+ designspace_name='TestVariableCOLR',
+ font_name='TestVariableCOLR',
+ tables=["GlyphOrder", "fvar", "glyf", "COLR", "CPAL"],
+ expected_ttx_name='TestVariableCOLR-VF',
+ save_before_dump=True,
+ )
+
def test_load_masters_layerName_without_required_font():
ds = DesignSpaceDocument()
s = SourceDescriptor()
diff --git a/Tests/varLib/varStore_test.py b/Tests/varLib/varStore_test.py
index a1bdd123..cad8ac73 100644
--- a/Tests/varLib/varStore_test.py
+++ b/Tests/varLib/varStore_test.py
@@ -13,7 +13,7 @@ from fontTools.ttLib.tables.otTables import VarStore
(
[{}, {"a": 1}],
[
- [10, 20],
+ [10, 10], # Test NO_VARIATION_INDEX
[100, 2000],
[100, 22000],
],
diff --git a/requirements.txt b/requirements.txt
index 008bc8c5..1bab6d77 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,13 +3,14 @@
brotli==1.0.9; platform_python_implementation != "PyPy"
brotlicffi==1.0.9.2; platform_python_implementation == "PyPy"
unicodedata2==14.0.0; python_version < '3.11'
-scipy==1.7.3; platform_python_implementation != "PyPy"
+scipy==1.7.3; platform_python_implementation != "PyPy" and python_version <= '3.7' # pyup: ignore
+scipy==1.9.0; platform_python_implementation != "PyPy" and python_version > '3.7'
munkres==1.1.4; platform_python_implementation == "PyPy"
-zopfli==0.1.9
-fs==2.4.14
+zopfli==0.2.1
+fs==2.4.16
skia-pathops==0.7.2; platform_python_implementation != "PyPy"
# this is only required to run Tests/cu2qu/{ufo,cli}_test.py
-ufoLib2==0.13.0
-pyobjc==8.1; sys_platform == "darwin"
-freetype-py==2.2.0
-uharfbuzz==0.24.1
+ufoLib2==0.13.1
+pyobjc==8.5; sys_platform == "darwin"
+freetype-py==2.3.0
+uharfbuzz==0.30.0
diff --git a/setup.cfg b/setup.cfg
index e184ce32..69bfde14 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.33.3
+current_version = 4.37.1
commit = True
tag = False
tag_name = {new_version}
@@ -52,6 +52,8 @@ filterwarnings =
ignore:writePlist:DeprecationWarning:plistlib_test
ignore:some_function:DeprecationWarning:fontTools.ufoLib.utils
ignore::DeprecationWarning:fontTools.varLib.designspace
+markers =
+ slow: marks tests as slow (deselect with '-m "not slow"')
[tool:interrogate]
ignore-semiprivate = true
diff --git a/setup.py b/setup.py
index 6e6d3912..03395ac5 100755
--- a/setup.py
+++ b/setup.py
@@ -68,6 +68,12 @@ if with_cython is True or (with_cython is None and has_cython):
ext_modules.append(
Extension("fontTools.cu2qu.cu2qu", ["Lib/fontTools/cu2qu/cu2qu.py"]),
)
+ ext_modules.append(
+ Extension("fontTools.pens.momentsPen", ["Lib/fontTools/pens/momentsPen.py"]),
+ )
+ ext_modules.append(
+ Extension("fontTools.varLib.iup", ["Lib/fontTools/varLib/iup.py"]),
+ )
extras_require = {
# for fontTools.ufoLib: to read/write UFO fonts
@@ -443,7 +449,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.33.3",
+ version="4.37.1",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",