diff options
author | Elliott Hughes <enh@google.com> | 2022-09-01 01:42:37 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-09-01 01:42:37 +0000 |
commit | 26b56f9a652565acf41409a3d773a0c4395de3ec (patch) | |
tree | e2eb0b9f93f0a8cb6705199d2f738f35cba28e7f | |
parent | debb1cba85fe0ed630b67e5d4a6d0ec5d7ef0284 (diff) | |
parent | e47f00b7523ba5d28bc3654ef83125d8e7dc0e90 (diff) | |
download | fonttools-26b56f9a652565acf41409a3d773a0c4395de3ec.tar.gz |
Upgrade fonttools to 4.37.1 am: ae8de171b8 am: ae308ea7c8 am: 14ad209ddd am: e47f00b752
Original change: https://android-review.googlesource.com/c/platform/external/fonttools/+/2193410
Change-Id: Ic4849ad3e570990227150b2420d6dd19fa0ad96f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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() @@ -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 } } @@ -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 Binary files differnew file mode 100644 index 00000000..119c24c8 --- /dev/null +++ b/Tests/ttLib/data/I.ttf 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 @@ -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 @@ -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", |