aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 05:08:23 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 05:08:23 +0000
commit9f770677d470d95687b0e086d5a5086832d843d2 (patch)
tree9448f0d8e2fabdd010330dfb5491f1bcef442a90
parentb7aeb240095054d117efd4754a2e010520e19868 (diff)
parent7fe79ddd4ba95175b23fc4bdd321b745cf142643 (diff)
downloadfonttools-android14-mainline-sdkext-release.tar.gz
Snap for 10453563 from 7fe79ddd4ba95175b23fc4bdd321b745cf142643 to mainline-sdkext-releaseaml_sdk_341510000aml_sdk_341410000aml_sdk_341110080aml_sdk_341110000aml_sdk_341010000aml_sdk_340912010android14-mainline-sdkext-release
Change-Id: Iaa8d97f851339d777b2b5464acf890aa4ae2991d
-rw-r--r--.codecov.yml11
-rw-r--r--.github/dependabot.yml6
-rw-r--r--.github/workflows/publish.yml7
-rw-r--r--.github/workflows/test.yml31
-rw-r--r--.gitignore3
-rw-r--r--Doc/README.md6
-rw-r--r--Doc/docs-requirements.txt6
-rw-r--r--Doc/source/config.rst8
-rw-r--r--Doc/source/designspaceLib/index.rst221
-rw-r--r--Doc/source/designspaceLib/python.rst199
-rw-r--r--Doc/source/designspaceLib/readme.rst1150
-rw-r--r--Doc/source/designspaceLib/scripting.rst49
-rw-r--r--Doc/source/designspaceLib/v5_class_diagram.pngbin0 -> 273627 bytes
-rw-r--r--Doc/source/designspaceLib/v5_class_diagram.puml292
-rw-r--r--Doc/source/designspaceLib/v5_split_downconvert.pngbin0 -> 99688 bytes
-rw-r--r--Doc/source/designspaceLib/v5_split_downconvert.puml147
-rw-r--r--Doc/source/designspaceLib/v5_variable_fonts_vs_instances.pngbin0 -> 144005 bytes
-rw-r--r--Doc/source/designspaceLib/xml.rst1017
-rw-r--r--Doc/source/index.rst4
-rw-r--r--Doc/source/misc/configTools.rst8
-rw-r--r--Doc/source/misc/index.rst1
-rw-r--r--Lib/fontTools/Android.bp5
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/cffLib/__init__.py11
-rw-r--r--Lib/fontTools/cffLib/specializer.py37
-rw-r--r--Lib/fontTools/cffLib/width.py4
-rw-r--r--Lib/fontTools/colorLib/builder.py155
-rw-r--r--Lib/fontTools/colorLib/unbuilder.py12
-rw-r--r--Lib/fontTools/config/__init__.py59
-rw-r--r--Lib/fontTools/designspaceLib/__init__.py1853
-rw-r--r--Lib/fontTools/designspaceLib/split.py435
-rw-r--r--Lib/fontTools/designspaceLib/statNames.py233
-rw-r--r--Lib/fontTools/designspaceLib/types.py147
-rw-r--r--Lib/fontTools/feaLib/builder.py4
-rw-r--r--Lib/fontTools/feaLib/parser.py36
-rw-r--r--Lib/fontTools/fontBuilder.py15
-rw-r--r--Lib/fontTools/merge/__init__.py5
-rw-r--r--Lib/fontTools/merge/cmap.py4
-rw-r--r--Lib/fontTools/merge/tables.py2
-rw-r--r--Lib/fontTools/misc/cliTools.py9
-rw-r--r--Lib/fontTools/misc/configTools.py348
-rw-r--r--Lib/fontTools/misc/psCharStrings.py16
-rw-r--r--Lib/fontTools/misc/symfont.py63
-rw-r--r--Lib/fontTools/misc/testTools.py28
-rw-r--r--Lib/fontTools/misc/treeTools.py45
-rw-r--r--Lib/fontTools/misc/visitor.py143
-rw-r--r--Lib/fontTools/mtiLib/__init__.py4
-rw-r--r--Lib/fontTools/otlLib/builder.py21
-rw-r--r--Lib/fontTools/otlLib/optimize/__init__.py41
-rw-r--r--Lib/fontTools/otlLib/optimize/gpos.py65
-rw-r--r--Lib/fontTools/pens/basePen.py3
-rw-r--r--Lib/fontTools/pens/cairoPen.py26
-rw-r--r--Lib/fontTools/pens/momentsPen.py662
-rw-r--r--Lib/fontTools/pens/qtPen.py4
-rw-r--r--Lib/fontTools/pens/statisticsPen.py12
-rw-r--r--Lib/fontTools/pens/svgPathPen.py79
-rw-r--r--Lib/fontTools/subset/__init__.py48
-rw-r--r--Lib/fontTools/subset/cff.py11
-rw-r--r--Lib/fontTools/subset/svg.py17
-rw-r--r--Lib/fontTools/svgLib/path/parser.py9
-rw-r--r--Lib/fontTools/ttLib/scaleUpem.py336
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_D_T_.py7
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_L_C_.py6
-rw-r--r--Lib/fontTools/ttLib/tables/O_S_2f_2.py14
-rw-r--r--Lib/fontTools/ttLib/tables/S_V_G_.py62
-rw-r--r--Lib/fontTools/ttLib/tables/_c_m_a_p.py25
-rw-r--r--Lib/fontTools/ttLib/tables/_g_l_y_f.py4
-rw-r--r--Lib/fontTools/ttLib/tables/_g_v_a_r.py63
-rw-r--r--Lib/fontTools/ttLib/tables/_k_e_r_n.py4
-rw-r--r--Lib/fontTools/ttLib/tables/otBase.py313
-rw-r--r--Lib/fontTools/ttLib/tables/otConverters.py112
-rwxr-xr-xLib/fontTools/ttLib/tables/otData.py106
-rw-r--r--Lib/fontTools/ttLib/tables/otTables.py60
-rw-r--r--Lib/fontTools/ttLib/tables/otTraverse.py137
-rw-r--r--Lib/fontTools/ttLib/ttFont.py172
-rw-r--r--Lib/fontTools/ttLib/ttGlyphSet.py221
-rw-r--r--Lib/fontTools/ttLib/ttVisitor.py32
-rwxr-xr-xLib/fontTools/ufoLib/__init__.py5
-rwxr-xr-xLib/fontTools/ufoLib/glifLib.py7
-rw-r--r--Lib/fontTools/varLib/__init__.py128
-rw-r--r--Lib/fontTools/varLib/cff.py1
-rw-r--r--Lib/fontTools/varLib/errors.py59
-rw-r--r--Lib/fontTools/varLib/featureVars.py16
-rw-r--r--Lib/fontTools/varLib/instancer/__init__.py161
-rw-r--r--Lib/fontTools/varLib/interpolatable.py184
-rw-r--r--Lib/fontTools/varLib/iup.py240
-rw-r--r--Lib/fontTools/varLib/merger.py472
-rw-r--r--Lib/fontTools/varLib/models.py162
-rw-r--r--Lib/fontTools/varLib/mutator.py5
-rw-r--r--Lib/fontTools/varLib/stat.py142
-rw-r--r--Lib/fontTools/varLib/varStore.py24
-rw-r--r--METADATA8
-rw-r--r--NEWS.rst226
-rw-r--r--Snippets/print-json.py153
-rw-r--r--Tests/colorLib/builder_test.py84
-rw-r--r--Tests/colorLib/unbuilder_test.py24
-rw-r--r--Tests/config_test.py135
-rw-r--r--Tests/designspaceLib/__init__.py0
-rw-r--r--Tests/designspaceLib/data/DS5BreakTest.designspace56
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_Wght.designspace114
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_WghtWdth.designspace316
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Wght.designspace103
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdth.designspace307
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdthItal.designspace670
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Italic.designspace326
-rw-r--r--Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Roman.designspace334
-rw-r--r--Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_Wght.designspace96
-rw-r--r--Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_WghtWdth.designspace262
-rw-r--r--Tests/designspaceLib/data/split_output/AktivGroteskVF_Wght.designspace85
-rw-r--r--Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdth.designspace253
-rw-r--r--Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdthItal.designspace562
-rw-r--r--Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight.designspace55
-rw-r--r--Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight_Width.designspace129
-rw-r--r--Tests/designspaceLib/data/split_output/MutatorSansVariable_Width.designspace50
-rw-r--r--Tests/designspaceLib/data/split_output/MutatorSerifVariable_Width.designspace29
-rw-r--r--Tests/designspaceLib/data/split_output/SourceSerif4Variable-Italic.designspace266
-rw-r--r--Tests/designspaceLib/data/split_output/SourceSerif4Variable-Roman.designspace274
-rw-r--r--Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_0.0.designspace147
-rw-r--r--Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_1.0.designspace44
-rw-r--r--Tests/designspaceLib/data/split_output/test_v5_aktiv_.designspace595
-rw-r--r--Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_0.0.designspace282
-rw-r--r--Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_1.0.designspace274
-rw-r--r--Tests/designspaceLib/data/test_v4_original.designspace (renamed from Tests/designspaceLib/data/test.designspace)9
-rw-r--r--Tests/designspaceLib/data/test_v5.designspace304
-rw-r--r--Tests/designspaceLib/data/test_v5_MutatorSans_and_Serif.designspace206
-rw-r--r--Tests/designspaceLib/data/test_v5_aktiv.designspace621
-rw-r--r--Tests/designspaceLib/data/test_v5_decovar.designspace242
-rw-r--r--Tests/designspaceLib/data/test_v5_discrete.designspace139
-rw-r--r--Tests/designspaceLib/data/test_v5_original.designspace90
-rw-r--r--Tests/designspaceLib/data/test_v5_sourceserif.designspace646
-rw-r--r--Tests/designspaceLib/designspace_test.py74
-rw-r--r--Tests/designspaceLib/designspace_v5_test.py904
-rw-r--r--Tests/designspaceLib/fixtures.py8
-rw-r--r--Tests/designspaceLib/split_test.py150
-rw-r--r--Tests/designspaceLib/statNames_test.py78
-rw-r--r--Tests/feaLib/data/name.fea12
-rw-r--r--Tests/feaLib/data/name.ttx18
-rw-r--r--Tests/feaLib/data/spec6h_ii.ttx24
-rw-r--r--Tests/feaLib/data/spec6h_iii_3d.ttx20
-rw-r--r--Tests/feaLib/parser_test.py2
-rw-r--r--Tests/fontBuilder/fontBuilder_test.py16
-rw-r--r--Tests/merge/data/CFFFont_expected.ttx50
-rw-r--r--Tests/misc/configTools_test.py80
-rw-r--r--Tests/misc/psCharStrings_test.py9
-rw-r--r--Tests/misc/treeTools_test.py80
-rw-r--r--Tests/misc/visitor_test.py72
-rw-r--r--Tests/otlLib/optimize_test.py39
-rw-r--r--Tests/pens/cu2quPen_test.py9
-rw-r--r--Tests/subset/data/expect_harfbuzz_repacker.ttx78
-rw-r--r--Tests/subset/data/expect_lcar_0.ttx2
-rw-r--r--Tests/subset/data/expect_lcar_1.ttx2
-rw-r--r--Tests/subset/data/expect_no_notdef_outline_cid.ttx2
-rw-r--r--Tests/subset/data/expect_notdef_width_cid.ttx2
-rw-r--r--Tests/subset/data/expect_opbd_0.ttx2
-rw-r--r--Tests/subset/data/expect_opbd_1.ttx2
-rw-r--r--Tests/subset/data/expect_prop_0.ttx2
-rw-r--r--Tests/subset/data/expect_prop_1.ttx2
-rw-r--r--Tests/subset/data/harfbuzz_repacker.ttx1542
-rw-r--r--Tests/subset/subset_test.py479
-rw-r--r--Tests/svgLib/path/parser_test.py11
-rw-r--r--Tests/ttLib/data/I-512upem.ttx3777
-rw-r--r--Tests/ttLib/data/I.ttfbin0 -> 13200 bytes
-rw-r--r--Tests/ttLib/scaleUpem_test.py75
-rw-r--r--Tests/ttLib/tables/C_O_L_R_test.py26
-rw-r--r--Tests/ttLib/tables/S_V_G__test.py37
-rw-r--r--Tests/ttLib/ttGlyphSet_test.py112
-rw-r--r--Tests/ttLib/ttVisitor_test.py39
-rw-r--r--Tests/varLib/data/TestVariableCOLR.designspace18
-rw-r--r--Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx357
-rw-r--r--Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx335
-rw-r--r--Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx (renamed from Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx)24
-rw-r--r--Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx (renamed from Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx)24
-rw-r--r--Tests/varLib/data/test_results/TestVariableCOLR-VF.ttx220
-rw-r--r--Tests/varLib/instancer/data/STATInstancerTest.ttx1830
-rw-r--r--Tests/varLib/instancer/instancer_test.py43
-rw-r--r--Tests/varLib/interpolate_layout_test.py12
-rw-r--r--Tests/varLib/iup_test.py53
-rw-r--r--Tests/varLib/merger_test.py1844
-rw-r--r--Tests/varLib/models_test.py332
-rw-r--r--Tests/varLib/stat_test.py167
-rw-r--r--Tests/varLib/varLib_test.py12
-rw-r--r--Tests/varLib/varStore_test.py2
-rw-r--r--requirements.txt14
-rw-r--r--setup.cfg4
-rwxr-xr-xsetup.py12
185 files changed, 29834 insertions, 2941 deletions
diff --git a/.codecov.yml b/.codecov.yml
index 5e7474d3..40928b93 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -1,5 +1,10 @@
comment: false
+
coverage:
- status:
- project: off
- patch: off
+ status:
+ project:
+ default:
+ informational: true
+ patch:
+ default:
+ informational: true
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/.gitignore b/.gitignore
index 9c564fd5..e62c33dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,3 +58,6 @@ Lib/**/*.c
# Ctags
tags
+
+# Documentation
+Doc/source/_build
diff --git a/Doc/README.md b/Doc/README.md
index 26641b72..6927bd62 100644
--- a/Doc/README.md
+++ b/Doc/README.md
@@ -1,6 +1,6 @@
# fontTools Documentation
-The fontTools project documentation updates continuously on Read the Docs as the project source changes.
+The fontTools project documentation updates continuously on Read the Docs as the project source changes.
The documentation is hosted at https://fonttools.readthedocs.io/.
@@ -19,7 +19,7 @@ You must have a Python 3 interpreter and the `pip` Python package manager instal
Pull the fontTools project source files, create a Python virtual environment, and then install fontTools and the documentation build dependencies by executing the following commands in the root of the fontTools source repository:
```
-$ pip install -e . [all]
+$ pip install -e .[all]
$ pip install -r Doc/docs-requirements.txt
```
@@ -112,7 +112,7 @@ Build a local set of HTML documentation files with the instructions above and re
### Submit a Pull Request
-Submit a Github pull request with your proposed improvements to the documentation.
+Submit a Github pull request with your proposed improvements to the documentation.
Thanks for your contribution!
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/config.rst b/Doc/source/config.rst
new file mode 100644
index 00000000..6be575d5
--- /dev/null
+++ b/Doc/source/config.rst
@@ -0,0 +1,8 @@
+###########################
+config: configure fontTools
+###########################
+
+.. automodule:: fontTools.config
+ :inherited-members:
+ :members:
+ :undoc-members:
diff --git a/Doc/source/designspaceLib/index.rst b/Doc/source/designspaceLib/index.rst
index 2c33df7b..5d17dc16 100644
--- a/Doc/source/designspaceLib/index.rst
+++ b/Doc/source/designspaceLib/index.rst
@@ -1,18 +1,217 @@
-##############
-designspaceLib
-##############
+#######################################################
+designspaceLib: Read, write, and edit designspace files
+#######################################################
-MutatorMath started out with its own reader and writer for designspaces.
-Since then the use of designspace has broadened and it would be useful
-to have a reader and writer that are independent of a specific system.
+Implements support for reading and manipulating ``designspace`` files.
+Allows the users to define axes, rules, sources, variable fonts and instances,
+and their STAT information.
.. toctree::
:maxdepth: 1
- readme
+ python
+ xml
scripting
-.. automodule:: fontTools.designspaceLib
- :inherited-members:
- :members:
- :undoc-members:
+
+Notes
+=====
+
+Paths and filenames
+-------------------
+
+A designspace file needs to store many references to UFO files.
+
+- designspace files can be part of versioning systems and appear on
+ different computers. This means it is not possible to store absolute
+ paths.
+- So, all paths are relative to the designspace document path.
+- Using relative paths allows designspace files and UFO files to be
+ **near** each other, and that they can be **found** without enforcing
+ one particular structure.
+- The **filename** attribute in the ``SourceDescriptor`` and
+ ``InstanceDescriptor`` classes stores the preferred relative path.
+- The **path** attribute in these objects stores the absolute path. It
+ is calculated from the document path and the relative path in the
+ filename attribute when the object is created.
+- Only the **filename** attribute is written to file.
+- Both **filename** and **path** must use forward slashes (``/``) as
+ path separators, even on Windows.
+
+Right before we save we need to identify and respond to the following
+situations:
+
+In each descriptor, we have to do the right thing for the filename
+attribute. Before writing to file, the ``documentObject.updatePaths()``
+method prepares the paths as follows:
+
+**Case 1**
+
+::
+
+ descriptor.filename == None
+ descriptor.path == None
+
+**Action**
+
+- write as is, descriptors will not have a filename attr. Useless, but
+ no reason to interfere.
+
+**Case 2**
+
+::
+
+ descriptor.filename == "../something"
+ descriptor.path == None
+
+**Action**
+
+- write as is. The filename attr should not be touched.
+
+**Case 3**
+
+::
+
+ descriptor.filename == None
+ descriptor.path == "~/absolute/path/there"
+
+**Action**
+
+- calculate the relative path for filename. We're not overwriting some
+ other value for filename, it should be fine.
+
+**Case 4**
+
+::
+
+ descriptor.filename == '../somewhere'
+ descriptor.path == "~/absolute/path/there"
+
+**Action**
+
+- There is a conflict between the given filename, and the path. The
+ difference could have happened for any number of reasons. Assuming
+ the values were not in conflict when the object was created, either
+ could have changed. We can't guess.
+- Assume the path attribute is more up to date. Calculate a new value
+ for filename based on the path and the document path.
+
+Recommendation for editors
+--------------------------
+
+- If you want to explicitly set the **filename** attribute, leave the
+ path attribute empty.
+- If you want to explicitly set the **path** attribute, leave the
+ filename attribute empty. It will be recalculated.
+- Use ``documentObject.updateFilenameFromPath()`` to explicitly set the
+ **filename** attributes for all instance and source descriptors.
+
+
+Common Lib Key Registry
+=======================
+
+public.skipExportGlyphs
+-----------------------
+
+This lib key works the same as the UFO lib key with the same name. The
+difference is that applications using a Designspace as the corner stone of the
+font compilation process should use the lib key in that Designspace instead of
+any of the UFOs. If the lib key is empty or not present in the Designspace, all
+glyphs should be exported, regardless of what the same lib key in any of the
+UFOs says.
+
+
+Implementation and differences
+==============================
+
+The designspace format has gone through considerable development.
+
+ - the format was originally written for MutatorMath.
+ - the format is now also used in fontTools.varlib.
+ - not all values are be required by all implementations.
+
+Varlib vs. MutatorMath
+----------------------
+
+There are some differences between the way MutatorMath and fontTools.varlib handle designspaces.
+
+ - Varlib does not support anisotropic interpolations.
+ - MutatorMath will extrapolate over the boundaries of
+ the axes. Varlib can not (at the moment).
+ - Varlib requires much less data to define an instance than
+ MutatorMath.
+ - The goals of Varlib and MutatorMath are different, so not all
+ attributes are always needed.
+
+
+Rules and generating static UFO instances
+-----------------------------------------
+
+When making instances as UFOs from a designspace with rules, it can
+be useful to evaluate the rules so that the characterset of the UFO
+reflects, as much as possible, the state of a variable font when seen
+at the same location. This can be done by some swapping and renaming of
+glyphs.
+
+While useful for proofing or development work, it should be noted that
+swapping and renaming leaves the UFOs with glyphnames that are no longer
+descriptive. For instance, after a swap ``dollar.bar`` could contain a shape
+without a bar. Also, when the swapped glyphs are part of other GSUB variations
+it can become complex very quickly. So proceed with caution.
+
+ - Assuming ``rulesProcessingLast = True``:
+ - We need to swap the glyphs so that the original shape is still available.
+ For instance, if a rule swaps ``a`` for ``a.alt``, a glyph
+ that references ``a`` in a component would then show the new ``a.alt``.
+ - But that can lead to unexpected results, the two glyphs may have different
+ widths or height. So, glyphs that are not specifically referenced in a rule
+ **should not change appearance**. That means that the implementation that swaps
+ ``a`` and ``a.alt`` also swap all components that reference these
+ glyphs in order to preserve their appearance.
+ - The swap function also needs to take care of swapping the names in
+ kerning data and any GPOS code.
+
+Version history
+===============
+
+Version 5.0
+-----------
+
+The format was extended to describe the entire design space of a reasonably
+regular font family in one file, with global data about the family to reduce
+repetition in sub-sections. "Reasonably regular" means that the sources and
+instances across the previously multiple Designspace files are positioned on a
+grid and derive their metadata (like style name) in a way that's compatible with
+the STAT model, based on their axis positions. Axis mappings must be the same
+across the entire space.
+
+1. Each axis can have labels attached to stops within the axis range, analogous to the
+ `OpenType STAT <https://docs.microsoft.com/en-us/typography/opentype/spec/stat>`_
+ table. Free-standing labels for locations are also allowed. The data is intended
+ to be compiled into a ``STAT`` table.
+2. The axes can be discrete, to say that they do not interpolate, like a distinctly
+ constructed upright and italic variant of a family.
+3. The data can be used to derive style and PostScript names for instances.
+4. A new ``variable-fonts`` element can subdivide the Designspace into multiple subsets that
+ mix and match the globally available axes. It is possible for these sub-spaces to have
+ a different default location from the global default location. It is required if the
+ Designspace contains a discrete axis and you want to produce a variable font.
+
+What is currently not supported is e.g.
+
+1. A setup where different sources sit at the same logical location in the design space,
+ think "MyFont Regular" and "MyFont SmallCaps Regular". (this situation could be
+ encoded by adding a "SmallCaps" discrete axis, if that makes sense).
+2. Anisotropic locations for axis labels.
+
+Older versions
+--------------
+
+- In some implementations that preceed Variable Fonts, the `copyInfo`
+ flag in a source indicated the source was to be treated as the default.
+ This is no longer compatible with the assumption that the default font
+ is located on the default value of each axis.
+- Older implementations did not require axis records to be present in
+ the designspace file. The axis extremes for instance were generated
+ from the locations used in the sources. This is no longer possible.
+
diff --git a/Doc/source/designspaceLib/python.rst b/Doc/source/designspaceLib/python.rst
new file mode 100644
index 00000000..6a43bdcc
--- /dev/null
+++ b/Doc/source/designspaceLib/python.rst
@@ -0,0 +1,199 @@
+################################
+1 DesignSpaceDocument Python API
+################################
+
+An object to read, write and edit interpolation systems for typefaces.
+Define sources, axes, rules, variable fonts and instances.
+
+Get an overview of the available classes in the Class Diagram below:
+
+.. figure:: v5_class_diagram.png
+ :width: 650px
+ :alt: UML class diagram of designspaceLib
+
+ UML class diagram of designspaceLib. Click to enlarge.
+
+.. contents:: Table of contents
+ :local:
+
+.. _designspacedocument-object:
+
+===================
+DesignSpaceDocument
+===================
+
+.. autoclass:: fontTools.designspaceLib::DesignSpaceDocument
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+.. _axis-descriptor-object:
+
+AxisDescriptor
+==============
+
+.. autoclass:: fontTools.designspaceLib::AxisDescriptor
+ :members:
+ :undoc-members:
+ :inherited-members: SimpleDescriptor
+ :member-order: bysource
+
+
+DiscreteAxisDescriptor
+======================
+
+.. autoclass:: fontTools.designspaceLib::DiscreteAxisDescriptor
+ :members:
+ :undoc-members:
+ :inherited-members: SimpleDescriptor
+ :member-order: bysource
+
+
+AxisLabelDescriptor
+-------------------
+
+.. autoclass:: fontTools.designspaceLib::AxisLabelDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+LocationLabelDescriptor
+=======================
+
+.. autoclass:: fontTools.designspaceLib::LocationLabelDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+RuleDescriptor
+==============
+
+.. autoclass:: fontTools.designspaceLib::RuleDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+Evaluating rules
+----------------
+
+.. autofunction:: fontTools.designspaceLib::evaluateRule
+.. autofunction:: fontTools.designspaceLib::evaluateConditions
+.. autofunction:: fontTools.designspaceLib::processRules
+
+
+.. _source-descriptor-object:
+
+SourceDescriptor
+================
+
+.. autoclass:: fontTools.designspaceLib::SourceDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+VariableFontDescriptor
+======================
+
+.. autoclass:: fontTools.designspaceLib::VariableFontDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+RangeAxisSubsetDescriptor
+-------------------------
+
+.. autoclass:: fontTools.designspaceLib::RangeAxisSubsetDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+ValueAxisSubsetDescriptor
+-------------------------
+
+.. autoclass:: fontTools.designspaceLib::ValueAxisSubsetDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+.. _instance-descriptor-object:
+
+InstanceDescriptor
+==================
+
+.. autoclass:: fontTools.designspaceLib::InstanceDescriptor
+ :members:
+ :undoc-members:
+ :member-order: bysource
+
+
+.. _subclassing-descriptors:
+
+=======================
+Subclassing descriptors
+=======================
+
+The DesignSpaceDocument can take subclassed Reader and Writer objects.
+This allows you to work with your own descriptors. You could subclass
+the descriptors. But as long as they have the basic attributes the
+descriptor does not need to be a subclass.
+
+.. code:: python
+
+ class MyDocReader(BaseDocReader):
+ axisDescriptorClass = MyAxisDescriptor
+ discreteAxisDescriptorClass = MyDiscreteAxisDescriptor
+ axisLabelDescriptorClass = MyAxisLabelDescriptor
+ locationLabelDescriptorClass = MyLocationLabelDescriptor
+ ruleDescriptorClass = MyRuleDescriptor
+ sourceDescriptorClass = MySourceDescriptor
+ variableFontsDescriptorClass = MyVariableFontDescriptor
+ valueAxisSubsetDescriptorClass = MyValueAxisSubsetDescriptor
+ rangeAxisSubsetDescriptorClass = MyRangeAxisSubsetDescriptor
+ instanceDescriptorClass = MyInstanceDescriptor
+
+ class MyDocWriter(BaseDocWriter):
+ axisDescriptorClass = MyAxisDescriptor
+ discreteAxisDescriptorClass = MyDiscreteAxisDescriptor
+ axisLabelDescriptorClass = MyAxisLabelDescriptor
+ locationLabelDescriptorClass = MyLocationLabelDescriptor
+ ruleDescriptorClass = MyRuleDescriptor
+ sourceDescriptorClass = MySourceDescriptor
+ variableFontsDescriptorClass = MyVariableFontDescriptor
+ valueAxisSubsetDescriptorClass = MyValueAxisSubsetDescriptor
+ rangeAxisSubsetDescriptorClass = MyRangeAxisSubsetDescriptor
+ instanceDescriptorClass = MyInstanceDescriptor
+
+ myDoc = DesignSpaceDocument(MyDocReader, MyDocWriter)
+
+
+==============
+Helper modules
+==============
+
+fontTools.designspaceLib.split
+==============================
+
+See :ref:`Scripting > Working with DesignSpace version 5 <working_with_v5>`
+for more information.
+
+.. automodule:: fontTools.designspaceLib.split
+
+
+fontTools.designspaceLib.stat
+=============================
+
+.. automodule:: fontTools.designspaceLib.stat
+
+
+fontTools.designspaceLib.statNames
+==================================
+
+.. automodule:: fontTools.designspaceLib.statNames
diff --git a/Doc/source/designspaceLib/readme.rst b/Doc/source/designspaceLib/readme.rst
deleted file mode 100644
index b9ba85a6..00000000
--- a/Doc/source/designspaceLib/readme.rst
+++ /dev/null
@@ -1,1150 +0,0 @@
-#################################
-DesignSpaceDocument Specification
-#################################
-
-An object to read, write and edit interpolation systems for typefaces. Define sources, axes, rules and instances.
-
-- `The Python API of the objects <#python-api>`_
-- `The document XML structure <#document-xml-structure>`_
-
-
-**********
-Python API
-**********
-
-
-
-.. _designspacedocument-object:
-
-DesignSpaceDocument object
-==========================
-
-The DesignSpaceDocument object can read and write ``.designspace`` data.
-It imports the axes, sources and instances to very basic **descriptor**
-objects that store the data in attributes. Data is added to the document
-by creating such descriptor objects, filling them with data and then
-adding them to the document. This makes it easy to integrate this object
-in different contexts.
-
-The **DesignSpaceDocument** object can be subclassed to work with
-different objects, as long as they have the same attributes. Reader and
-Writer objects can be subclassed as well.
-
-**Note:** Python attribute names are usually camelCased, the
-corresponding `XML <#document-xml-structure>`_ attributes are usually
-all lowercase.
-
-.. example-1:
-
-.. code:: python
-
- from fontTools.designspaceLib import DesignSpaceDocument
- doc = DesignSpaceDocument()
- doc.read("some/path/to/my.designspace")
- doc.axes
- doc.sources
- doc.instances
-
-Attributes
-----------
-
-- ``axes``: list of axisDescriptors
-- ``sources``: list of sourceDescriptors
-- ``instances``: list of instanceDescriptors
-- ``rules``: list if ruleDescriptors
-- ``readerClass``: class of the reader object
-- ``writerClass``: class of the writer object
-- ``lib``: dict for user defined, custom data that needs to be stored
- in the designspace. Use reverse-DNS notation to identify your own data.
- Respect the data stored by others.
-- ``rulesProcessingLast``: This flag indicates whether the substitution rules should be applied before or after other glyph substitution features. False: before, True: after.
-
-Methods
--------
-
-- ``read(path)``: read a designspace file from ``path``
-- ``write(path)``: write this designspace to ``path``
-- ``addSource(aSourceDescriptor)``: add this sourceDescriptor to
- ``doc.sources``.
-- ``addInstance(anInstanceDescriptor)``: add this instanceDescriptor
- to ``doc.instances``.
-- ``addAxis(anAxisDescriptor)``: add this instanceDescriptor to ``doc.axes``.
-- ``newDefaultLocation()``: returns a dict with the default location
- in designspace coordinates.
-- ``updateFilenameFromPath(masters=True, instances=True, force=False)``:
- set a descriptor filename attr from the path and this document.
-- ``newAxisDescriptor()``: return a new axisDescriptor object.
-- ``newSourceDescriptor()``: return a new sourceDescriptor object.
-- ``newInstanceDescriptor()``: return a new instanceDescriptor object.
-- ``getAxisOrder()``: return a list of axisnames
-- ``findDefault()``: return the sourceDescriptor that is on the default
- location. Returns None if there isn't one.
-- ``normalizeLocation(aLocation)``: return a dict with normalized axis values.
-- ``normalize()``: normalize the geometry of this designspace: scale all the
- locations of all masters and instances to the ``-1 - 0 - 1`` value.
-- ``loadSourceFonts()``: Ensure SourceDescriptor.font attributes are loaded,
- and return list of fonts.
-- ``tostring(encoding=None)``: Returns the designspace as a string. Default
- encoding `utf-8`.
-
-Class Methods
--------------
-- ``fromfile(path)``
-- ``fromstring(string)``
-
-
-
-
-.. _source-descriptor-object:
-
-SourceDescriptor object
-=======================
-
-Attributes
-----------
-
-- ``filename``: string. A relative path to the source file, **as it is
- in the document**. MutatorMath + Varlib.
-- ``path``: string. Absolute path to the source file, calculated from
- the document path and the string in the filename attr. MutatorMath +
- Varlib.
-- ``layerName``: string. The name of the layer in the source to look for
- outline data. Default ``None`` which means ``foreground``.
-- ``font``: Any Python object. Optional. Points to a representation of
- this source font that is loaded in memory, as a Python object
- (e.g. a ``defcon.Font`` or a ``fontTools.ttFont.TTFont``). The default
- document reader will not fill-in this attribute, and the default
- writer will not use this attribute. It is up to the user of
- ``designspaceLib`` to either load the resource identified by ``filename``
- and store it in this field, or write the contents of this field to the
- disk and make ``filename`` point to that.
-- ``name``: string. Optional. Unique identifier name for this source,
- if there is one or more ``instance.glyph`` elements in the document.
- MutatorMath.
-- ``location``: dict. Axis values for this source. MutatorMath + Varlib
-- ``copyLib``: bool. Indicates if the contents of the font.lib need to
- be copied to the instances. MutatorMath.
-- ``copyInfo`` bool. Indicates if the non-interpolating font.info needs
- to be copied to the instances. MutatorMath
-- ``copyGroups`` bool. Indicates if the groups need to be copied to the
- instances. MutatorMath.
-- ``copyFeatures`` bool. Indicates if the feature text needs to be
- copied to the instances. MutatorMath.
-- ``muteKerning``: bool. Indicates if the kerning data from this source
- needs to be muted (i.e. not be part of the calculations).
- MutatorMath.
-- ``muteInfo``: bool. Indicated if the interpolating font.info data for
- this source needs to be muted. MutatorMath.
-- ``mutedGlyphNames``: list. Glyphnames that need to be muted in the
- instances. MutatorMath.
-- ``familyName``: string. Family name of this source. Though this data
- can be extracted from the font, it can be efficient to have it right
- here. Varlib.
-- ``styleName``: string. Style name of this source. Though this data
- can be extracted from the font, it can be efficient to have it right
- here. Varlib.
-
-.. code:: python
-
- doc = DesignSpaceDocument()
- s1 = SourceDescriptor()
- s1.path = masterPath1
- s1.name = "master.ufo1"
- s1.font = defcon.Font("master.ufo1")
- s1.copyLib = True
- s1.copyInfo = True
- s1.copyFeatures = True
- s1.location = dict(weight=0)
- s1.familyName = "MasterFamilyName"
- s1.styleName = "MasterStyleNameOne"
- s1.mutedGlyphNames.append("A")
- s1.mutedGlyphNames.append("Z")
- doc.addSource(s1)
-
-.. _instance-descriptor-object:
-
-InstanceDescriptor object
-=========================
-
-.. attributes-1:
-
-
-Attributes
-----------
-
-- ``filename``: string. Relative path to the instance file, **as it is
- in the document**. The file may or may not exist. MutatorMath.
-- ``path``: string. Absolute path to the source file, calculated from
- the document path and the string in the filename attr. The file may
- or may not exist. MutatorMath.
-- ``name``: string. Unique identifier name of the instance, used to
- identify it if it needs to be referenced from elsewhere in the
- document.
-- ``location``: dict. Axis values for this source. MutatorMath +
- Varlib.
-- ``familyName``: string. Family name of this instance. MutatorMath +
- Varlib.
-- ``localisedFamilyName``: dict. A dictionary of localised family name
- strings, keyed by language code.
-- ``styleName``: string. Style name of this source. MutatorMath +
- Varlib.
-- ``localisedStyleName``: dict. A dictionary of localised stylename
- strings, keyed by language code.
-- ``postScriptFontName``: string. Postscript fontname for this
- instance. MutatorMath.
-- ``styleMapFamilyName``: string. StyleMap familyname for this
- instance. MutatorMath.
-- ``localisedStyleMapFamilyName``: A dictionary of localised style map
- familyname strings, keyed by language code.
-- ``localisedStyleMapStyleName``: A dictionary of localised style map
- stylename strings, keyed by language code.
-- ``styleMapStyleName``: string. StyleMap stylename for this instance.
- MutatorMath.
-- ``glyphs``: dict for special master definitions for glyphs. If glyphs
- need special masters (to record the results of executed rules for
- example). MutatorMath.
-- ``kerning``: bool. Indicates if this instance needs its kerning
- calculated. MutatorMath.
-- ``info``: bool. Indicated if this instance needs the interpolating
- font.info calculated.
-- ``lib``: dict. Custom data associated with this instance.
-
-Methods
--------
-
-These methods give easier access to the localised names.
-
-- ``setStyleName(styleName, languageCode="en")``
-- ``getStyleName(languageCode="en")``
-- ``setFamilyName(familyName, languageCode="en")``
-- ``getFamilyName(self, languageCode="en")``
-- ``setStyleMapStyleName(styleMapStyleName, languageCode="en")``
-- ``getStyleMapStyleName(languageCode="en")``
-- ``setStyleMapFamilyName(styleMapFamilyName, languageCode="en")``
-- ``getStyleMapFamilyName(languageCode="en")``
-
-Example
--------
-
-.. code:: python
-
- i2 = InstanceDescriptor()
- i2.path = instancePath2
- i2.familyName = "InstanceFamilyName"
- i2.styleName = "InstanceStyleName"
- i2.name = "instance.ufo2"
- # anisotropic location
- i2.location = dict(weight=500, width=(400,300))
- i2.postScriptFontName = "InstancePostscriptName"
- i2.styleMapFamilyName = "InstanceStyleMapFamilyName"
- i2.styleMapStyleName = "InstanceStyleMapStyleName"
- glyphMasters = [dict(font="master.ufo1", glyphName="BB", location=dict(width=20,weight=20)), dict(font="master.ufo2", glyphName="CC", location=dict(width=900,weight=900))]
- glyphData = dict(name="arrow", unicodeValue=1234)
- glyphData['masters'] = glyphMasters
- glyphData['note'] = "A note about this glyph"
- glyphData['instanceLocation'] = dict(width=100, weight=120)
- i2.glyphs['arrow'] = glyphData
- i2.glyphs['arrow2'] = dict(mute=False)
- i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever'
- doc.addInstance(i2)
-
-.. _axis-descriptor-object:
-
-AxisDescriptor object
-=====================
-
-- ``tag``: string. Four letter tag for this axis. Some might be
- registered at the `OpenType
- specification <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__.
- Privately-defined axis tags must begin with an uppercase letter and
- use only uppercase letters or digits.
-- ``name``: string. Name of the axis as it is used in the location
- dicts. MutatorMath + Varlib.
-- ``labelNames``: dict. When defining a non-registered axis, it will be
- necessary to define user-facing readable names for the axis. Keyed by
- xml:lang code. Values are required to be ``unicode`` strings, even if
- they only contain ASCII characters.
-- ``minimum``: number. The minimum value for this axis in user space.
- MutatorMath + Varlib.
-- ``maximum``: number. The maximum value for this axis in user space.
- MutatorMath + Varlib.
-- ``default``: number. The default value for this axis, i.e. when a new
- location is created, this is the value this axis will get in user
- space. MutatorMath + Varlib.
-- ``map``: list of input / output values that can describe a warp
- of user space to design space coordinates. If no map values are present, it is assumed user space is the same as design space, as
- in [(minimum, minimum), (maximum, maximum)]. Varlib.
-
-.. code:: python
-
- a1 = AxisDescriptor()
- a1.minimum = 1
- a1.maximum = 1000
- a1.default = 400
- a1.name = "weight"
- a1.tag = "wght"
- a1.labelNames[u'fa-IR'] = u"قطر"
- a1.labelNames[u'en'] = u"Wéíght"
- a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)]
-
-RuleDescriptor object
-=====================
-
-- ``name``: string. Unique name for this rule. Can be used to
- reference this rule data.
-- ``conditionSets``: a list of conditionsets
-- Each conditionset is a list of conditions.
-- Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys.
-- ``subs``: list of substitutions
-- Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt").
-- Note: By default, rules are applied first, before other text shaping/OpenType layout, as they are part of the `Required Variation Alternates OpenType feature <https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#-tag-rvrn>`_. See `5.0 rules element`_ § Attributes.
-
-Evaluating rules
-----------------
-
-- ``evaluateRule(rule, location)``: Return True if any of the rule's conditionsets
- matches the given location.
-- ``evaluateConditions(conditions, location)``: Return True if all the conditions
- matches the given location.
-- ``processRules(rules, location, glyphNames)``: Apply all the rules to the list
- of glyphNames. Return a new list of glyphNames with substitutions applied.
-
-.. code:: python
-
- r1 = RuleDescriptor()
- r1.name = "unique.rule.name"
- r1.conditionSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)])
- r1.conditionSets.append([dict(...), dict(...)])
- r1.subs.append(("a", "a.alt"))
-
-
-.. _subclassing-descriptors:
-
-Subclassing descriptors
-=======================
-
-The DesignSpaceDocument can take subclassed Reader and Writer objects.
-This allows you to work with your own descriptors. You could subclass
-the descriptors. But as long as they have the basic attributes the
-descriptor does not need to be a subclass.
-
-.. code:: python
-
- class MyDocReader(BaseDocReader):
- ruleDescriptorClass = MyRuleDescriptor
- axisDescriptorClass = MyAxisDescriptor
- sourceDescriptorClass = MySourceDescriptor
- instanceDescriptorClass = MyInstanceDescriptor
-
- class MyDocWriter(BaseDocWriter):
- ruleDescriptorClass = MyRuleDescriptor
- axisDescriptorClass = MyAxisDescriptor
- sourceDescriptorClass = MySourceDescriptor
- instanceDescriptorClass = MyInstanceDescriptor
-
- myDoc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter)
-
-**********************
-Document xml structure
-**********************
-
-- The ``axes`` element contains one or more ``axis`` elements.
-- The ``sources`` element contains one or more ``source`` elements.
-- The ``instances`` element contains one or more ``instance`` elements.
-- The ``rules`` element contains one or more ``rule`` elements.
-- The ``lib`` element contains arbitrary data.
-
-.. code:: xml
-
- <?xml version='1.0' encoding='utf-8'?>
- <designspace format="3">
- <axes>
- <!-- define axes here -->
- <axis../>
- </axes>
- <sources>
- <!-- define masters here -->
- <source../>
- </sources>
- <instances>
- <!-- define instances here -->
- <instance../>
- </instances>
- <rules>
- <!-- define rules here -->
- <rule../>
- </rules>
- <lib>
- <dict>
- <!-- store custom data here -->
- </dict>
- </lib>
- </designspace>
-
-.. 1-axis-element:
-
-1. axis element
-===============
-
-- Define a single axis
-- Child element of ``axes``
-
-.. attributes-2:
-
-Attributes
-----------
-
-- ``name``: required, string. Name of the axis that is used in the
- location elements.
-- ``tag``: required, string, 4 letters. Some axis tags are registered
- in the OpenType Specification.
-- ``minimum``: required, number. The minimum value for this axis, in user space coordinates.
-- ``maximum``: required, number. The maximum value for this axis, in user space coordinates.
-- ``default``: required, number. The default value for this axis, in user space coordinates.
-- ``hidden``: optional, 0 or 1. Records whether this axis needs to be
- hidden in interfaces.
-
-.. code:: xml
-
- <axis name="weight" tag="wght" minimum="1" maximum="1000" default="400">
-
-.. 11-labelname-element:
-
-1.1 labelname element
-=====================
-
-- Defines a human readable name for UI use.
-- Optional for non-registered axis names.
-- Can be localised with ``xml:lang``
-- Child element of ``axis``
-
-.. attributes-3:
-
-Attributes
-----------
-
-- ``xml:lang``: required, string. `XML language
- definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
-
-Value
------
-
-- The natural language name of this axis.
-
-.. example-2:
-
-Example
--------
-
-.. code:: xml
-
- <labelname xml:lang="fa-IR">قطر</labelname>
- <labelname xml:lang="en">Wéíght</labelname>
-
-.. 12-map-element:
-
-1.2 map element
-===============
-
-- Defines a single node in a series of input value (user space coordinate)
- to output value (designspace coordinate) pairs.
-- Together these values transform the designspace.
-- Child of ``axis`` element.
-
-.. example-3:
-
-Example
--------
-
-.. code:: xml
-
- <map input="1.0" output="10.0" />
- <map input="400.0" output="66.0" />
- <map input="1000.0" output="990.0" />
-
-Example of all axis elements together:
---------------------------------------
-
-.. code:: xml
-
- <axes>
- <axis default="1" maximum="1000" minimum="0" name="weight" tag="wght">
- <labelname xml:lang="fa-IR">قطر</labelname>
- <labelname xml:lang="en">Wéíght</labelname>
- </axis>
- <axis default="100" maximum="200" minimum="50" name="width" tag="wdth">
- <map input="50.0" output="10.0" />
- <map input="100.0" output="66.0" />
- <map input="200.0" output="990.0" />
- </axis>
- </axes>
-
-.. 2-location-element:
-
-2. location element
-===================
-
-- Defines a coordinate in the design space.
-- Dictionary of axisname: axisvalue
-- Used in ``source``, ``instance`` and ``glyph`` elements.
-
-.. 21-dimension-element:
-
-2.1 dimension element
-=====================
-
-- Child element of ``location``
-
-.. attributes-4:
-
-Attributes
-----------
-
-- ``name``: required, string. Name of the axis.
-- ``xvalue``: required, number. The value on this axis.
-- ``yvalue``: optional, number. Separate value for anisotropic
- interpolations.
-
-.. example-4:
-
-Example
--------
-
-.. code:: xml
-
- <location>
- <dimension name="width" xvalue="0.000000" />
- <dimension name="weight" xvalue="0.000000" yvalue="0.003" />
- </location>
-
-.. 3-source-element:
-
-3. source element
-=================
-
-- Defines a single font or layer that contributes to the designspace.
-- Child element of ``sources``
-- Location in designspace coordinates.
-
-.. attributes-5:
-
-Attributes
-----------
-
-- ``familyname``: optional, string. The family name of the source font.
- While this could be extracted from the font data itself, it can be
- more efficient to add it here.
-- ``stylename``: optional, string. The style name of the source font.
-- ``name``: required, string. A unique name that can be used to
- identify this font if it needs to be referenced elsewhere.
-- ``filename``: required, string. A path to the source file, relative
- to the root path of this document. The path can be at the same level
- as the document or lower.
-- ``layer``: optional, string. The name of the layer in the source file.
- If no layer attribute is given assume the foreground layer should be used.
-
-.. 31-lib-element:
-
-3.1 lib element
-===============
-
-There are two meanings for the ``lib`` element:
-
-1. Source lib
- - Example: ``<lib copy="1" />``
- - Child element of ``source``
- - Defines if the instances can inherit the data in the lib of this
- source.
- - MutatorMath only
-
-2. Document and instance lib
- - Example:
-
- .. code:: xml
-
- <lib>
- <dict>
- <key>...</key>
- <string>The contents use the PLIST format.</string>
- </dict>
- </lib>
-
- - Child element of ``designspace`` and ``instance``
- - Contains arbitrary data about the whole document or about a specific
- instance.
- - Items in the dict need to use **reverse domain name notation** <https://en.wikipedia.org/wiki/Reverse_domain_name_notation>__
-
-.. 32-info-element:
-
-3.2 info element
-================
-
-- ``<info copy="1" />``
-- Child element of ``source``
-- Defines if the instances can inherit the non-interpolating font info
- from this source.
-- MutatorMath
-
-.. 33-features-element:
-
-3.3 features element
-====================
-
-- ``<features copy="1" />``
-- Defines if the instances can inherit opentype feature text from this
- source.
-- Child element of ``source``
-- MutatorMath only
-
-.. 34-glyph-element:
-
-3.4 glyph element
-=================
-
-- Can appear in ``source`` as well as in ``instance`` elements.
-- In a ``source`` element this states if a glyph is to be excluded from
- the calculation.
-- MutatorMath only
-
-.. attributes-6:
-
-Attributes
-----------
-
-- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph
- should be ignored as a master.
-- ``<glyph mute="1" name="A"/>``
-- MutatorMath only
-
-.. 35-kerning-element:
-
-3.5 kerning element
-===================
-
-- ``<kerning mute="1" />``
-- Can appear in ``source`` as well as in ``instance`` elements.
-
-.. attributes-7:
-
-Attributes
-----------
-
-- ``mute``: required attribute, number 1 or 0. Indicate if the kerning
- data from this source is to be excluded from the calculation.
-- If the kerning element is not present, assume ``mute=0``, yes,
- include the kerning of this source in the calculation.
-- MutatorMath only
-
-.. example-5:
-
-Example
--------
-
-.. code:: xml
-
- <source familyname="MasterFamilyName" filename="masters/masterTest1.ufo" name="master.ufo1" stylename="MasterStyleNameOne">
- <lib copy="1" />
- <features copy="1" />
- <info copy="1" />
- <glyph mute="1" name="A" />
- <glyph mute="1" name="Z" />
- <location>
- <dimension name="width" xvalue="0.000000" />
- <dimension name="weight" xvalue="0.000000" />
- </location>
- </source>
-
-.. 4-instance-element:
-
-4. instance element
-===================
-
-- Defines a single font that can be calculated with the designspace.
-- Child element of ``instances``
-- For use in Varlib the instance element really only needs the names
- and the location. The ``glyphs`` element is not required.
-- MutatorMath uses the ``glyphs`` element to describe how certain
- glyphs need different masters, mainly to describe the effects of
- conditional rules in Superpolator.
-- Location in designspace coordinates.
-
-.. attributes-8:
-
-Attributes
-----------
-
-- ``familyname``: required, string. The family name of the instance
- font. Corresponds with ``font.info.familyName``
-- ``stylename``: required, string. The style name of the instance font.
- Corresponds with ``font.info.styleName``
-- ``name``: required, string. A unique name that can be used to
- identify this font if it needs to be referenced elsewhere.
-- ``filename``: string. Required for MutatorMath. A path to the
- instance file, relative to the root path of this document. The path
- can be at the same level as the document or lower.
-- ``postscriptfontname``: string. Optional for MutatorMath. Corresponds
- with ``font.info.postscriptFontName``
-- ``stylemapfamilyname``: string. Optional for MutatorMath. Corresponds
- with ``styleMapFamilyName``
-- ``stylemapstylename``: string. Optional for MutatorMath. Corresponds
- with ``styleMapStyleName``
-
-Example for varlib
-------------------
-
-.. code:: xml
-
- <instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
- <location>
- <dimension name="width" xvalue="400" yvalue="300" />
- <dimension name="weight" xvalue="66" />
- </location>
- <kerning />
- <info />
- <lib>
- <dict>
- <key>com.coolDesignspaceApp.specimenText</key>
- <string>Hamburgerwhatever</string>
- </dict>
- </lib>
- </instance>
-
-.. 41-glyphs-element:
-
-4.1 glyphs element
-==================
-
-- Container for ``glyph`` elements.
-- Optional
-- MutatorMath only.
-
-.. 42-glyph-element:
-
-4.2 glyph element
-=================
-
-- Child element of ``glyphs``
-- May contain a ``location`` element.
-
-.. attributes-9:
-
-Attributes
-----------
-
-- ``name``: string. The name of the glyph.
-- ``unicode``: string. Unicode values for this glyph, in hexadecimal.
- Multiple values should be separated with a space.
-- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph
- should be supressed in the output.
-
-.. 421-note-element:
-
-4.2.1 note element
-==================
-
-- String. The value corresponds to glyph.note in UFO.
-
-.. 422-masters-element:
-
-4.2.2 masters element
-=====================
-
-- Container for ``master`` elements
-- These ``master`` elements define an alternative set of glyph masters
- for this glyph.
-
-.. 4221-master-element:
-
-4.2.2.1 master element
-======================
-
-- Defines a single alternative master for this glyph.
-
-4.3 Localised names for instances
-=================================
-
-Localised names for instances can be included with these simple elements
-with an ``xml:lang`` attribute:
-`XML language definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
-
-- stylename
-- familyname
-- stylemapstylename
-- stylemapfamilyname
-
-.. example-6:
-
-Example
--------
-
-.. code:: xml
-
- <stylename xml:lang="fr">Demigras</stylename>
- <stylename xml:lang="ja">半ば</stylename>
- <familyname xml:lang="fr">Montserrat</familyname>
- <familyname xml:lang="ja">モンセラート</familyname>
- <stylemapstylename xml:lang="de">Standard</stylemapstylename>
- <stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
- <stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
-
-.. attributes-10:
-
-Attributes
-----------
-
-- ``glyphname``: the name of the alternate master glyph.
-- ``source``: the identifier name of the source this master glyph needs
- to be loaded from
-
-.. example-7:
-
-Example
--------
-
-.. code:: xml
-
- <instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
- <location>
- <dimension name="width" xvalue="400" yvalue="300" />
- <dimension name="weight" xvalue="66" />
- </location>
- <glyphs>
- <glyph name="arrow2" />
- <glyph name="arrow" unicode="0x4d2 0x4d3">
- <location>
- <dimension name="width" xvalue="100" />
- <dimension name="weight" xvalue="120" />
- </location>
- <note>A note about this glyph</note>
- <masters>
- <master glyphname="BB" source="master.ufo1">
- <location>
- <dimension name="width" xvalue="20" />
- <dimension name="weight" xvalue="20" />
- </location>
- </master>
- </masters>
- </glyph>
- </glyphs>
- <kerning />
- <info />
- <lib>
- <dict>
- <key>com.coolDesignspaceApp.specimenText</key>
- <string>Hamburgerwhatever</string>
- </dict>
- </lib>
- </instance>
-
-.. 50-rules-element:
-
-5.0 rules element
-=================
-
-- Container for ``rule`` elements
-- The rules are evaluated in this order.
-
-Rules describe designspace areas in which one glyph should be replaced by another.
-A rule has a name and a number of conditionsets. The rule also contains a list of
-glyphname pairs: the glyphs that need to be substituted. For a rule to be triggered
-**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset
-**all** conditions need to be true, ``AND``.
-
-.. attributes-11:
-
-Attributes
-----------
-
-- ``processing``: flag, optional. Valid values are [``first``, ``last``]. This flag indicates whether the substitution rules should be applied before or after other glyph substitution features.
-- If no ``processing`` attribute is given, interpret as ``first``, and put the substitution rule in the `rvrn` feature.
-- If ``processing`` is ``last``, put it in `rclt`.
-
-.. 51-rule-element:
-
-5.1 rule element
-================
-
-- Defines a named rule.
-- Each ``rule`` element contains one or more ``conditionset`` elements.
-- **Only one** ``conditionset`` needs to be true to trigger the rule.
-- **All** conditions in a ``conditionset`` must be true to make the ``conditionset`` true.
-- For backwards compatibility a ``rule`` can contain ``condition`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``conditionset``. Note: these conditions should be written wrapped in a conditionset.
-- A rule element needs to contain one or more ``sub`` elements in order to be compiled to a variable font.
-- Rules without sub elements should be ignored when compiling a font.
-- For authoring tools it might be necessary to save designspace files without ``sub`` elements just because the work is incomplete.
-
-.. attributes-11:
-
-Attributes
-----------
-
-- ``name``: optional, string. A unique name that can be used to
- identify this rule if it needs to be referenced elsewhere. The name
- is not important for compiling variable fonts.
-
-5.1.1 conditionset element
-==========================
-
-- Child element of ``rule``
-- Contains one or more ``condition`` elements.
-
-.. 512-condition-element:
-
-5.1.2 condition element
-=======================
-
-- Child element of ``conditionset``
-- Between the ``minimum`` and ``maximum`` this condition is ``True``.
-- ``minimum`` and ``maximum`` are in designspace coordinates.
-- If ``minimum`` is not available, assume it is ``axis.minimum``, mapped to designspace coordinates.
-- If ``maximum`` is not available, assume it is ``axis.maximum``, mapped to designspace coordinates.
-- The condition must contain at least a minimum or maximum or both.
-
-.. attributes-12:
-
-Attributes
-----------
-
-- ``name``: string, required. Must match one of the defined ``axis``
- name attributes.
-- ``minimum``: number, required*. The low value, in designspace coordinates.
-- ``maximum``: number, required*. The high value, in designspace coordinates.
-
-.. 513-sub-element:
-
-5.1.3 sub element
-=================
-
-- Child element of ``rule``.
-- Defines which glyph to replace when the rule evaluates to **True**.
-- The ``sub`` element contains a pair of glyphnames. The ``name`` attribute is the glyph that should be visible when the rule evaluates to **False**. The ``with`` attribute is the glyph that should be visible when the rule evaluates to **True**.
-
-Axis values in Conditions are in designspace coordinates.
-
-.. attributes-13:
-
-Attributes
-----------
-
-- ``name``: string, required. The name of the glyph this rule looks
- for.
-- ``with``: string, required. The name of the glyph it is replaced
- with.
-
-.. example-8:
-
-Example
--------
-
-Example with an implied ``conditionset``. Here the conditions are not
-contained in a conditionset.
-
-.. code:: xml
-
- <rules processing="last">
- <rule name="named.rule.1">
- <condition minimum="250" maximum="750" name="weight" />
- <condition minimum="50" maximum="100" name="width" />
- <sub name="dollar" with="dollar.alt"/>
- </rule>
- </rules>
-
-Example with ``conditionsets``. All conditions in a conditionset must be true.
-
-.. code:: xml
-
- <rules>
- <rule name="named.rule.2">
- <conditionset>
- <condition minimum="250" maximum="750" name="weight" />
- <condition minimum="50" maximum="100" name="width" />
- </conditionset>
- <conditionset>
- <condition ... />
- <condition ... />
- </conditionset>
- <sub name="dollar" with="dollar.alt"/>
- </rule>
- </rules>
-
-.. 6-notes:
-
-6 Notes
-=======
-
-Paths and filenames
--------------------
-
-A designspace file needs to store many references to UFO files.
-
-- designspace files can be part of versioning systems and appear on
- different computers. This means it is not possible to store absolute
- paths.
-- So, all paths are relative to the designspace document path.
-- Using relative paths allows designspace files and UFO files to be
- **near** each other, and that they can be **found** without enforcing
- one particular structure.
-- The **filename** attribute in the ``SourceDescriptor`` and
- ``InstanceDescriptor`` classes stores the preferred relative path.
-- The **path** attribute in these objects stores the absolute path. It
- is calculated from the document path and the relative path in the
- filename attribute when the object is created.
-- Only the **filename** attribute is written to file.
-- Both **filename** and **path** must use forward slashes (``/``) as
- path separators, even on Windows.
-
-Right before we save we need to identify and respond to the following
-situations:
-
-In each descriptor, we have to do the right thing for the filename
-attribute. Before writing to file, the ``documentObject.updatePaths()``
-method prepares the paths as follows:
-
-**Case 1**
-
-::
-
- descriptor.filename == None
- descriptor.path == None
-
-**Action**
-
-- write as is, descriptors will not have a filename attr. Useless, but
- no reason to interfere.
-
-**Case 2**
-
-::
-
- descriptor.filename == "../something"
- descriptor.path == None
-
-**Action**
-
-- write as is. The filename attr should not be touched.
-
-**Case 3**
-
-::
-
- descriptor.filename == None
- descriptor.path == "~/absolute/path/there"
-
-**Action**
-
-- calculate the relative path for filename. We're not overwriting some
- other value for filename, it should be fine.
-
-**Case 4**
-
-::
-
- descriptor.filename == '../somewhere'
- descriptor.path == "~/absolute/path/there"
-
-**Action**
-
-- There is a conflict between the given filename, and the path. The
- difference could have happened for any number of reasons. Assuming
- the values were not in conflict when the object was created, either
- could have changed. We can't guess.
-- Assume the path attribute is more up to date. Calculate a new value
- for filename based on the path and the document path.
-
-Recommendation for editors
---------------------------
-
-- If you want to explicitly set the **filename** attribute, leave the
- path attribute empty.
-- If you want to explicitly set the **path** attribute, leave the
- filename attribute empty. It will be recalculated.
-- Use ``documentObject.updateFilenameFromPath()`` to explicitly set the
- **filename** attributes for all instance and source descriptors.
-
-.. 7-common-lib-key-registry:
-
-7 Common Lib Key Registry
-=========================
-
-public.skipExportGlyphs
------------------------
-
-This lib key works the same as the UFO lib key with the same name. The
-difference is that applications using a Designspace as the corner stone of the
-font compilation process should use the lib key in that Designspace instead of
-any of the UFOs. If the lib key is empty or not present in the Designspace, all
-glyphs should be exported, regardless of what the same lib key in any of the
-UFOs says.
-
-.. 8-implementation-and-differences:
-
-
-8 Implementation and differences
-================================
-
-The designspace format has gone through considerable development.
-
- - the format was originally written for MutatorMath.
- - the format is now also used in fontTools.varlib.
- - not all values are be required by all implementations.
-
-8.1 Varlib vs. MutatorMath
---------------------------
-
-There are some differences between the way MutatorMath and fontTools.varlib handle designspaces.
-
- - Varlib does not support anisotropic interpolations.
- - MutatorMath will extrapolate over the boundaries of
- the axes. Varlib can not (at the moment).
- - Varlib requires much less data to define an instance than
- MutatorMath.
- - The goals of Varlib and MutatorMath are different, so not all
- attributes are always needed.
-
-8.2 Older versions
-------------------
-
-- In some implementations that preceed Variable Fonts, the `copyInfo`
- flag in a source indicated the source was to be treated as the default.
- This is no longer compatible with the assumption that the default font
- is located on the default value of each axis.
-- Older implementations did not require axis records to be present in
- the designspace file. The axis extremes for instance were generated
- from the locations used in the sources. This is no longer possible.
-
-8.3 Rules and generating static UFO instances
----------------------------------------------
-
-When making instances as UFOs from a designspace with rules, it can
-be useful to evaluate the rules so that the characterset of the ufo
-reflects, as much as possible, the state of a variable font when seen
-at the same location. This can be done by some swapping and renaming of
-glyphs.
-
-While useful for proofing or development work, it should be noted that
-swapping and renaming leaves the UFOs with glyphnames that are no longer
-descriptive. For instance, after a swap `dollar.bar` could contain a shape
-without a bar. Also, when the swapped glyphs are part of other GSUB variations
-it can become complex very quickly. So proceed with caution.
-
- - Assuming `rulesProcessingLast = True`:
- - We need to swap the glyphs so that the original shape is still available.
- For instance, if a rule swaps ``a`` for ``a.alt``, a glyph
- that references ``a`` in a component would then show the new ``a.alt``.
- - But that can lead to unexpected results, the two glyphs may have different
- widths or height. So, glyphs that are not specifically referenced in a rule
- **should not change appearance**. That means that the implementation that swaps
- ``a`` and ``a.alt`` also swap all components that reference these
- glyphs in order to preserve their appearance.
- - The swap function also needs to take care of swapping the names in
- kerning data and any GPOS code.
-
-
-.. 9-this-document
-
-9 This document
-===============
-
-- Changes are to be expected.
-
-
diff --git a/Doc/source/designspaceLib/scripting.rst b/Doc/source/designspaceLib/scripting.rst
index 5a17816a..63235eec 100644
--- a/Doc/source/designspaceLib/scripting.rst
+++ b/Doc/source/designspaceLib/scripting.rst
@@ -1,6 +1,6 @@
-#######################
-Scripting a designspace
-#######################
+#########################
+3 Scripting a designspace
+#########################
It can be useful to build a designspace with a script rather than
construct one with an interface like
@@ -60,7 +60,7 @@ Make a descriptor object and add it to the document.
Variation Axis
Tags <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__
- The default master is expected at the intersection of all
- default values of all axes.
+ default values of all axes.
Option: add label names
-----------------------
@@ -88,6 +88,7 @@ OpenType <https://www.microsoft.com/typography/otspec/avar.htm>`__.
.. code:: python
+ # (user space, design space), (user space, design space)...
a1.map = [(0.0, 10.0), (401.0, 66.0), (1000.0, 990.0)]
Make a source object
@@ -124,7 +125,7 @@ So go ahead and add another master:
s1.name = "master.bold"
s1.location = dict(weight=1000)
doc.addSource(s1)
-
+
Option: exclude glyphs
----------------------
@@ -241,7 +242,7 @@ This is how you check the default font.
Generating?
***********
-You can generate the UFO's with MutatorMath:
+You can generate the UFOs with MutatorMath:
.. code:: python
@@ -251,3 +252,39 @@ You can generate the UFO's with MutatorMath:
- Assuming the outline data in the masters is compatible.
Or you can use the file in making a **variable font** with varlib.
+
+
+.. _working_with_v5:
+
+**********************************
+Working with DesignSpace version 5
+**********************************
+
+The new version 5 allows "discrete" axes, which do not interpolate across their
+values. This is useful to store in one place family-wide data such as the STAT
+information, however it prevents the usual things done on designspaces that
+interpolate everywhere:
+
+- checking that all sources are compatible for interpolation
+- building variable fonts
+
+In order to allow the above in tools that want to handle designspace v5,
+the :mod:`fontTools.designspaceLib.split` module provides two methods to
+split a designspace into interpolable sub-spaces,
+:func:`splitInterpolable() <fontTools.designspaceLib.split.splitInterpolable>`
+and then
+:func:`splitVariableFonts() <fontTools.designspaceLib.split.splitVariableFonts>`.
+
+
+.. figure:: v5_split_downconvert.png
+ :width: 680px
+ :alt: Example process diagram to check and build DesignSpace 5
+
+ Example process process to check and build Designspace 5.
+
+
+Also, for older tools that don't know about the other version 5 additions such
+as the STAT data fields, the function
+:func:`convert5to4() <fontTools.designspaceLib.split.convert5to4>` allows to
+strip new information from a designspace version 5 to downgrade it to a
+collection of version 4 documents, one per variable font.
diff --git a/Doc/source/designspaceLib/v5_class_diagram.png b/Doc/source/designspaceLib/v5_class_diagram.png
new file mode 100644
index 00000000..7c75bcb9
--- /dev/null
+++ b/Doc/source/designspaceLib/v5_class_diagram.png
Binary files differ
diff --git a/Doc/source/designspaceLib/v5_class_diagram.puml b/Doc/source/designspaceLib/v5_class_diagram.puml
new file mode 100644
index 00000000..31f9e9c3
--- /dev/null
+++ b/Doc/source/designspaceLib/v5_class_diagram.puml
@@ -0,0 +1,292 @@
+@startuml v5_class_diagram
+
+title
+ Designspace v5 Class Diagram
+
+ <size:12>Note: the ""Descriptor"" suffix is omitted from most classes
+end title
+
+' left to right direction
+
+skinparam class {
+BackgroundColor<<New>> PaleGreen
+}
+
+class DesignSpaceDocument {
++ formatVersion: str = None
++ <color:green><b><<New>> elidedFallbackName: str = None
++ rulesProcessingLast: bool = False
++ lib: Dict = {}
+}
+
+note left of DesignSpaceDocument::elidedFallbackName
+STAT Style Attributes Header field ""elidedFallbackNameID""
+end note
+
+abstract class AbstractAxis {
++ tag: str
++ name: str
++ labelNames: Dict[str, str]
++ hidden: bool
++ map: List[Tuple[float, float]]
++ <color:green><b><<New>> axisOrdering: int
+}
+DesignSpaceDocument *-- "*" AbstractAxis: axes >
+note right of AbstractAxis::axisOrdering
+STAT Axis Record field
+end note
+
+class Axis {
++ minimum: float
++ maximum: float
++ default: float
+}
+AbstractAxis <|--- Axis
+note bottom of Axis
+This is the usual
+Axis, with a range
+of values.
+end note
+
+class DiscreteAxis <<New>> {
++ values: List[float]
++ default: float
+}
+AbstractAxis <|--- DiscreteAxis
+note bottom of DiscreteAxis
+A discrete axis is not
+interpolable, e.g.
+Uprights vs Italics, and
+so has "discrete" stops
+instead of a continuous
+range of values.
+end note
+
+Axis .[hidden] DiscreteAxis
+
+class AxisLabel <<New>> {
++ userMinimum: Optional[float]
++ userValue: float
++ userMaximum: Optional[float]
++ name: str
++ elidable: bool
++ olderSibling: bool
++ linkedUserValue: Optional[float]
++ labelNames: Dict[str, str]
+
++ getFormat(): 1 | 2 | 3
+}
+note right of AxisLabel
+Label for a
+stop on an Axis
+(STAT format
+1,2,3)
+end note
+AbstractAxis *-- "*" AxisLabel: <<New>> \n axisLabels >
+
+class LocationLabel <<New>> {
++ name: str
++ location: Dict[str, float]
++ elidable: bool
++ olderSibling: bool
++ labelNames: Dict[str, str]
+}
+note right of LocationLabel
+Label for a
+freestanding
+location
+(STAT format 4)
+end note
+DesignSpaceDocument *--- "*" LocationLabel: <<New>> \n locationLabels >
+
+class Rule {
++ name: str
++ conditionSets: List[ConditionSet]
++ subs: Dict[str, str]
+}
+DesignSpaceDocument *- "*" Rule: rules >
+
+class Source {
++ name: Optional[str]
++ filename: str
++ path: str
++ layerName: Optional[str]
++ <color:brown><s><<Deprecated>> location: Location
++ <color:green><b><<New>> designLocation: AnisotropicLocation
+....
++ font: Optional[Font]
+....
++ familyName: Optional[str]
++ styleName: Optional[str]
++ <color:green><b><<New>> localisedFamilyName: Dict
+....
++ <color:brown><s><<Deprecated>> copyLib: bool
++ <color:brown><s><<Deprecated>> copyInfo: bool
++ <color:brown><s><<Deprecated>> copyGroups: bool
++ <color:brown><s><<Deprecated>> copyFeatures: bool
+....
++ muteKerning: bool
++ muteInfo: bool
++ mutedGlyphNames: List[str]
+----
++ <color:green><b><<New>> getFullDesignLocation(doc)
+}
+DesignSpaceDocument *-- "*" Source: sources >
+note right of Source::localisedFamilyName
+New field to allow generation
+of localised instance names using
+STAT information.
+end note
+note right of Source::copyGroups
+These fields are already not meaningful
+anymore in version 4 (the default source
+will be used as "neutral" for groups, info
+and features. ''copyLib'' can be emulated
+by putting content in the designspace's lib.
+end note
+
+note as NLocSource
+The location of
+sources can still only
+be defined in design
+coordinates, and now
+also by relying on
+axis defaults.
+
+To build the final,
+"full" location, a
+helper method is
+provided, that uses
+axis defaults and
+axis mappings to
+fill in the blanks.
+end note
+NLocSource . Source::designLocation
+NLocSource . Source::getFullDesignLocation
+
+class VariableFont <<New>> {
++ filename: str
++ lib: Dict
+}
+DesignSpaceDocument *--- "*" VariableFont: <<New>> \n variableFonts >
+note right of VariableFont
+A variable font is a
+subset of the designspace
+where everything interpolates
+(and so can be compiled into
+an OpenType variable font).
+end note
+
+abstract class AbstractAxisSubset <<New>> {
++ name: str
+}
+VariableFont *-- "*" AbstractAxisSubset: <<New>> \n axisSubsets >
+
+note right of AbstractAxisSubset
+An axis subset selects a range
+or a spot on each the available
+axes from the whole designspace.
+
+By default, only the default value
+of each axis is used to define the
+variable font.
+
+Continuous axes can be specified
+to include their full range instead;
+or a subset of the range.
+
+Discrete axes can be specified
+to include a different spot than the
+default.
+end note
+
+class RangeAxisSubset <<New>> {
++ userMinimum: float
++ userDefault: float
++ userMaximum: float
+}
+AbstractAxisSubset <|-- RangeAxisSubset
+
+class ValueAxisSubset <<New>> {
++ userValue: float
+}
+AbstractAxisSubset <|-- ValueAxisSubset
+
+class Instance {
++ filename: str
++ path: str
++ <color:brown><s><<Deprecated>> location: Location
++ <color:green><b><<New>> locationLabel: str
++ <color:green><b><<New>> designLocation: AnisotropicLocation
++ <color:green><b><<New>> userLocation: SimpleLocation
+....
++ font: Optional[Font]
+....
++ <color:orange><b><<Changed>> name: Optional[str]
++ <color:orange><b><<Changed>> familyName: Optional[str]
++ <color:orange><b><<Changed>> styleName: Optional[str]
++ <color:orange><b><<Changed>> postScriptFontName: Optional[str]
++ <color:orange><b><<Changed>> styleMapFamilyName: Optional[str]
++ <color:orange><b><<Changed>> styleMapStyleName: Optional[str]
++ localisedFamilyName: Dict
++ localisedStyleName: Dict
++ localisedStyleMapFamilyName: Dict
++ localisedStyleMapStyleName: Dict
+....
++ <color:brown><s><<Deprecated>> glyphs: Dict
++ <color:brown><s><<Deprecated>> kerning: bool
++ <color:brown><s><<Deprecated>> info: bool
+....
++ lib: Dict
+----
++ <color:green><b><<New>> clearLocation()
++ <color:green><b><<New>> getLocationLabelDescriptor(doc)
++ <color:green><b><<New>> getFullDesignLocation(doc)
++ <color:green><b><<New>> getFullUserLocation(doc)
+}
+DesignSpaceDocument *-- "*" Instance: instances >
+note right of Instance::locationLabel
+The location can now alternatively
+be a string = name of a LocationLabel
+(STAT format 4). The instance then
+adopts the location of that label.
+See the Decovar example file.
+end note
+note right of Instance::styleMapStyleName
+All the name field are now optional.
+Their default values will be computed
+using the provided STAT table data
+and the STAT rules from the spec.
+For styleMap{Family,Style}Name, they
+would be computed using the STAT
+""linkedUserValue"" fields.
+end note
+note right of Instance::glyphs
+This attribute has been replaced by rules
+end note
+note right of Instance::kerning
+All instances get kerning and info
+nowadays.
+end note
+
+note as NLocInstance
+The location of instances
+can now be defined using
+either:
+- a STAT LocationLabel
+- design coordinates
+- user coordinates.
+- relying on axis defaults
+
+To build the final, "full"
+location, a few helper
+methods are provided,
+that aggregate data from
+these sources and apply
+the axis mappings.
+end note
+NLocInstance . Instance::designLocation
+NLocInstance . Instance::getFullDesignLocation
+
+@enduml
+
diff --git a/Doc/source/designspaceLib/v5_split_downconvert.png b/Doc/source/designspaceLib/v5_split_downconvert.png
new file mode 100644
index 00000000..8867378b
--- /dev/null
+++ b/Doc/source/designspaceLib/v5_split_downconvert.png
Binary files differ
diff --git a/Doc/source/designspaceLib/v5_split_downconvert.puml b/Doc/source/designspaceLib/v5_split_downconvert.puml
new file mode 100644
index 00000000..6f51f80c
--- /dev/null
+++ b/Doc/source/designspaceLib/v5_split_downconvert.puml
@@ -0,0 +1,147 @@
+@startuml v5_split_downconvert
+
+title
+How to split and down-convert a DesignSpace version 5
+<size:14>so that existing tools can work with it
+end title
+
+
+start
+
+#lightgrey:DesignSpace 5.0
+- with STAT data
+- optional instance names
+- with discrete axes
+- with multiple VFs]
+
+note left
+ - STAT data means that compilers will need
+ to write that data to the ouput TTFs
+ - optional instance names means that
+ compilers will need to generate any missing
+ names using the STAT data
+ - discrete axes mean that not all sources
+ are compatible for interpolation
+ - multiple VFs means that compilers will
+ need to output several TTFs for one DS
+end note
+
+split
+split again
+
+:""splitInterpolating()"";
+
+note left
+ - Create one DS document per interpolating
+ sub-space, for example: with a discrete
+ axis for Upright vs Italics, create 2
+ DesignSpaces, one for the Uprights, and
+ one for the Italics.
+ - Expand all missing instance names using
+ the STAT data.
+ - Drop all the STAT data because as we start
+ taking out discrete axes, the sub-documents
+ lose STAT data anyway (only the full
+ document at the start of the process should
+ be used to generate the STAT table)
+end note
+
+split
+
+ #lightgrey:DesignSpace 5.0
+ - (no STAT data)
+ - (explicit instance names)
+ - (at discrete location #1)
+ - with multiple VFs]
+
+ note left
+ All sources in this sub-space are
+ (supposed to be) compatible for
+ interpolation
+ end note
+
+split again
+
+ #lightgrey:DesignSpace 5.0
+ - (no STAT data)
+ - (explicit instance names)
+ - (at discrete location #2)
+ - with multiple VFs]
+ detach
+
+split again
+
+ #lightgrey:etc
+ ...]
+ detach
+
+end split
+
+:check compatibility;
+:build compatible master TTFs with cu2qu;
+
+:""splitVariableFonts()"";
+
+note left
+ Create one DS document per variable font,
+ for example: the above document may
+ describe a weight and width space, out of
+ which we'll build 3 variable fonts:
+ full weight + width, weight only, width only.
+end note
+
+split
+
+ #lightgrey:DesignSpace 5.0
+ - (no STAT data)
+ - (explicit instance names)
+ - (at discrete location #1)
+ - (describing just VF #1)]
+
+ note left
+ This document looks very much
+ like version 4.1, you can just
+ change the version number at
+ the top and feed it to a tool
+ that doesn't know about v5;
+ see ""convert5to4()"".
+ end note
+
+split again
+
+ #lightgrey:DesignSpace 5.0
+ - (no STAT data)
+ - (explicit instance names)
+ - (at discrete location #1)
+ - (describing just VF #2)]
+ detach
+
+split again
+
+ #lightgrey:etc
+ ...]
+ detach
+
+end split
+
+:""varLib.build()"";
+
+#lightgrey:Variable font TTF
+No STAT data]
+
+end split
+
+:""buildVFStatTable()"";
+
+note left
+Apply STAT data using the full document
+from the start of the process + filtering
+entries based on this VF's location
+end note
+
+#lightgrey:Variable font TTF
+With STAT data]
+
+end
+
+@enduml
diff --git a/Doc/source/designspaceLib/v5_variable_fonts_vs_instances.png b/Doc/source/designspaceLib/v5_variable_fonts_vs_instances.png
new file mode 100644
index 00000000..7a5915a3
--- /dev/null
+++ b/Doc/source/designspaceLib/v5_variable_fonts_vs_instances.png
Binary files differ
diff --git a/Doc/source/designspaceLib/xml.rst b/Doc/source/designspaceLib/xml.rst
new file mode 100644
index 00000000..6267b025
--- /dev/null
+++ b/Doc/source/designspaceLib/xml.rst
@@ -0,0 +1,1017 @@
+.. _document-xml-structure:
+
+**********************
+Document XML structure
+**********************
+
+.. sectnum::
+ :start: 2
+.. Note: impossible with Sphinx to avoid numbering the document title
+.. See this issue: https://github.com/sphinx-doc/sphinx/issues/4628
+
+.. contents:: Table of contents (levels match the document structure)
+ :local:
+
+========
+Overview
+========
+
+
+.. code:: xml
+
+ <?xml version='1.0' encoding='utf-8'?>
+ <designspace format="5.0">
+ <axes>
+ <!-- define axes here -->
+ <axis... />
+ </axes>
+ <labels>
+ <!-- define STAT format 4 labels here -->
+ <!-- New in version 5.0 -->
+ <label... />
+ </labels>
+ <sources>
+ <!-- define masters here -->
+ <source... />
+ </sources>
+ <variable-fonts>
+ <!-- define variable fonts here -->
+ <!-- New in version 5.0 -->
+ <variable-font... />
+ </variable-fonts>
+ <instances>
+ <!-- define instances here -->
+ <instance... />
+ </instances>
+ <rules>
+ <!-- define rules here -->
+ <rule... />
+ </rules>
+ <lib>
+ <dict>
+ <!-- store custom data here -->
+ </dict>
+ </lib>
+ </designspace>
+
+==================
+``<axes>`` element
+==================
+
+The ``<axes>`` element contains one or more ``<axis>`` elements.
+
+.. rubric:: Attributes
+
+- ``elidedfallbackname``: optional, string.
+ STAT Style Attributes Header field ``elidedFallbackNameID``.
+ See: `OTSpec STAT Style Attributes Header
+ <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#style-attributes-header>`_
+
+ .. versionadded:: 5.0
+
+
+``<axis>`` element
+==================
+
+- Define a single axis
+- Child element of ``axes``
+- The axis can be either continuous (as in version 4.0) or discrete (new in version 5.0).
+ Discrete axes have a list of values instead of a range minimum and maximum.
+
+
+.. rubric:: Attributes
+
+- ``name``: required, string. Name of the axis that is used in the
+ location elements.
+- ``tag``: required, string, 4 letters. Some axis tags are registered
+ in the OpenType Specification.
+- ``default``: required, number. The default value for this axis, in user space coordinates.
+- ``hidden``: optional, 0 or 1. Records whether this axis needs to be
+ hidden in interfaces.
+
+For a continuous axis:
+ - ``minimum``: required, number. The minimum value for this axis, in user space coordinates.
+ - ``maximum``: required, number. The maximum value for this axis, in user space coordinates.
+
+For a discrete axis:
+ - ``values``: required, space-separated numbers. The exhaustive list of possible values along this axis.
+
+ .. versionadded:: 5.0
+
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <axis name="weight" tag="wght" minimum="1" maximum="1000" default="400">
+
+ <!--
+ Discrete axes provide a list of discrete values.
+ No interpolation is allowed between these.
+ -->
+ <axis name="Italic" tag="ital" default="0" values="0 1">
+
+
+.. _labelname:
+
+``<labelname>`` element (axis)
+------------------------------
+
+- Defines a human readable name for UI use.
+- Optional for non-registered axis names.
+- Can be localised with ``xml:lang``
+- Child element of ``<axis>`` or ``<label>``
+
+
+.. rubric:: Attributes
+
+- ``xml:lang``: required, string. `XML language
+ definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
+
+
+.. rubric:: Value
+
+- The natural language name of this axis or STAT label.
+
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <labelname xml:lang="fa-IR">قطر</labelname>
+ <labelname xml:lang="en">Wéíght</labelname>
+
+
+``<map>`` element
+-----------------
+
+- Defines a single node in a series of input value (user space coordinate)
+ to output value (designspace coordinate) pairs.
+- Together these values transform the designspace.
+- Child of ``<axis>`` element.
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <map input="1.0" output="10.0" />
+ <map input="400.0" output="66.0" />
+ <map input="1000.0" output="990.0" />
+
+
+``<labels>`` element (axis)
+---------------------------
+
+The ``<labels>`` element contains one or more ``<label>`` elements.
+
+.. versionadded:: 5.0
+
+
+``<label>`` element (axis)
+..........................
+
+- Define STAT format 1, 2, 3 labels for the locations on this axis.
+- The axis can have several child ``<label>`` elements, one for each STAT entry.
+- This ``<label>`` element can have several ``<labelname>`` child elements,
+ to provide translations of its ``name`` attribute.
+
+.. versionadded:: 5.0
+
+.. rubric:: Attributes
+
+- ``name``: required, string. the name of this label
+- ``elidable``: optional, boolean, default: false. STAT flag ``ELIDABLE_AXIS_VALUE_NAME``.
+- ``oldersibling``: optional, boolean, default: false. STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``.
+
+ See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
+
+Depending on the intended target STAT format, use a combination of the following
+field, all in user coordinates. Check the following table for the format
+correspondences.
+
+- ``uservalue``: (required) STAT field ``value`` (format 1, 3) or ``nominalValue`` (format 2).
+- ``userminimum``: STAT field ``rangeMinValue`` (format 2).
+- ``usermaximum``: STAT field ``rangeMaxValue`` (format 2).
+- ``linkeduservalue``: STAT field ``linkedValue`` (format 3).
+
+=========== ========= =========== =========== ===============
+STAT Format uservalue userminimum usermaximum linkeduservalue
+=========== ========= =========== =========== ===============
+1 ✅ ❌ ❌ ❌
+2 ✅ ✅ ✅ ❌
+3 ✅ ❌ ❌ ✅
+=========== ========= =========== =========== ===============
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <label userminimum="200" uservalue="200" usermaximum="250" name="Extra Light">
+ <labelname xml:lang="de">Extraleicht</labelname>
+ <labelname xml:lang="fr">Extra léger</labelname>
+ </label>
+ <label userminimum="350" uservalue="400" usermaximum="450" name="Regular" elidable="true" />
+
+
+``<labelname>`` element (axis STAT label)
+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+
+User-facing translations of this STAT label. Keyed by ``xml:lang`` code.
+
+.. versionadded:: 5.0
+
+Same attribute and value as :ref:`the axis' \<labelname\> element <labelname>`.
+
+
+Example of all axis elements together
+=====================================
+
+.. code:: xml
+
+ <axes elidedfallbackname="Regular">
+ <axis default="1" maximum="1000" minimum="0" name="weight" tag="wght">
+ <labelname xml:lang="fa-IR">قطر</labelname>
+ <labelname xml:lang="en">Wéíght</labelname>
+ <labels>
+ <label userminimum="200" uservalue="200" usermaximum="250" name="Extra Light">
+ <labelname xml:lang="de">Extraleicht</labelname>
+ <labelname xml:lang="fr">Extra léger</labelname>
+ </label>
+ <label userminimum="350" uservalue="400" usermaximum="450" name="Regular" elidable="true" />
+ </labels>
+ </axis>
+ <axis default="100" maximum="200" minimum="50" name="width" tag="wdth">
+ <map input="50.0" output="10.0" />
+ <map input="100.0" output="66.0" />
+ <map input="200.0" output="990.0" />
+ </axis>
+ </axes>
+
+
+================================
+``<labels>`` element (top-level)
+================================
+
+The ``<labels>`` element contains one or more ``<label>`` elements.
+
+.. versionadded:: 5.0
+
+``<label>`` element (top-level)
+===============================
+
+- Define STAT format 4 labels for a free-standing location.
+- The designspace can have several top-level ``<label>`` elements, one for each
+ STAT format 4 entry.
+- This ``<label>`` element must have a child ``<location>`` element that
+ represents the location to which the label applies.
+- This ``<label>`` element may have several child ``<labelname>`` elements to
+ provide translations of its ``name`` attribute.
+
+
+See: `OTSpec STAT Axis value table, format 4 <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4>`_
+
+.. versionadded:: 5.0
+
+.. rubric:: Attributes
+
+- ``name``: required, string. the name of this label
+- ``elidable``: optional, boolean, default: false. STAT flag ``ELIDABLE_AXIS_VALUE_NAME``.
+- ``oldersibling``: optional, boolean, default: false. STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``.
+
+ See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
+
+
+.. _location:
+
+``<location>`` element (top-level STAT label)
+---------------------------------------------
+
+- Defines a coordinate in either user or design space.
+- Encodes a dictionary of ``{ axisname: axisvalue }``.
+- Also used in ``<source>``, ``<instance>`` and ``<glyph>`` elements.
+- This ``<location>`` element must have one or more child ``<dimension>``
+ elements.
+
+.. _dimension:
+
+``<dimension>`` element
+.......................
+
+- Child element of ``<location>``
+
+.. rubric:: Attributes
+
+- ``name``: required, string. Name of the axis.
+
+Depending on whether you're representing a location in user or design coordinates,
+provide one of the attributes below.
+
+For user-space coordinates:
+
+- ``uservalue``: required, number. The value on this axis in user coordinates.
+
+ .. versionadded:: 5.0
+
+For design-space coordinates:
+
+- ``xvalue``: required, number. The value on this axis in design coordinates.
+- ``yvalue``: optional, number. Separate value for anisotropic interpolations.
+
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <location>
+ <dimension name="Width" uservalue="125" />
+ <dimension name="Weight" xvalue="10" yvalue="20.5" />
+ </location>
+
+
+``<labelname>`` element (top-level STAT label)
+----------------------------------------------
+
+User-facing translations of this STAT label. Keyed by ``xml:lang`` code.
+
+.. versionadded:: 5.0
+
+Same attribute and value as :ref:`the axis' \<labelname\> element <labelname>`.
+
+
+.. _rules-element:
+
+===================
+``<rules>`` element
+===================
+
+The ``<rules>`` element contains one or more ``<rule>`` elements.
+
+The rules are evaluated in this order.
+
+Rules describe designspace areas in which one glyph should be replaced by another.
+A rule has a name and a number of conditionsets. The rule also contains a list of
+glyphname pairs: the glyphs that need to be substituted. For a rule to be triggered
+**only one** of the conditionsets needs to be true, ``OR``. Within a conditionset
+**all** conditions need to be true, ``AND``.
+
+
+.. rubric:: Attributes
+
+- ``processing``: flag, optional. Valid values are [``first``, ``last``]. This
+ flag indicates whether the substitution rules should be applied before or after
+ other glyph substitution features.
+
+ - If no ``processing`` attribute is given, interpret as ``first``, and put
+ the substitution rule in the ``rvrn`` feature.
+ - If ``processing`` is ``last``, put it in ``rclt``.
+ - The default is ``first``. For new projects, you probably want ``last``.
+ See the following issues for more information:
+ `fontTools#1371 <https://github.com/fonttools/fonttools/issues/1371#issuecomment-590214572>`__
+ `fontTools#2050 <https://github.com/fonttools/fonttools/issues/2050#issuecomment-678691020>`__
+ - If you want to use a different feature altogether, e.g. ``calt``,
+ use the lib key ``com.github.fonttools.varLib.featureVarsFeatureTag``
+
+ .. code:: xml
+
+ <lib>
+ <dict>
+ <key>com.github.fonttools.varLib.featureVarsFeatureTag</key>
+ <string>calt</string>
+ </dict>
+ </lib>
+
+
+
+``<rule>`` element
+==================
+
+- Defines a named rule.
+- Each ``<rule>`` element contains one or more ``<conditionset>`` elements.
+- **Only one** ``<conditionset>`` needs to be true to trigger the rule (logical OR).
+- **All** conditions in a ``<conditionset>`` must be true to make the ``<conditionset>`` true. (logical AND)
+- For backwards compatibility a ``<rule>`` can contain ``<condition>`` elements outside of a conditionset. These are then understood to be part of a single, implied, ``<conditionset>``. Note: these conditions should be written wrapped in a conditionset.
+- A rule element needs to contain one or more ``<sub>`` elements in order to be compiled to a variable font.
+- Rules without sub elements should be ignored when compiling a font.
+- For authoring tools it might be necessary to save designspace files without ``<sub>`` elements just because the work is incomplete.
+
+
+.. rubric:: Attributes
+
+- ``name``: optional, string. A unique name that can be used to
+ identify this rule if it needs to be referenced elsewhere. The name
+ is not important for compiling variable fonts.
+
+``<conditionset>`` element
+--------------------------
+
+- Child element of ``<rule>``
+- Contains one or more ``<condition>`` elements.
+
+
+``<condition>`` element
+.......................
+
+- Child element of ``<conditionset>``
+- Between the ``minimum`` and ``maximum`` this condition is ``True``.
+- ``minimum`` and ``maximum`` are in designspace coordinates.
+- If ``minimum`` is not available, assume it is ``axis.minimum``, mapped to designspace coordinates.
+- If ``maximum`` is not available, assume it is ``axis.maximum``, mapped to designspace coordinates.
+- The condition must contain at least a minimum or maximum or both.
+
+
+.. rubric:: Attributes
+
+- ``name``: string, required. Must match one of the defined ``axis``
+ name attributes.
+- ``minimum``: number, required*. The low value, in design coordinates.
+- ``maximum``: number, required*. The high value, in design coordinates.
+
+.. If you want to specify the condition limits in design coordinates:
+
+.. If you want to specify the condition limits in user coordinates:
+
+.. - ``userminimum``: number, required*. The low value, in design coordinates.
+.. - ``usermaximum``: number, required*. The high value, in design coordinates.
+
+``<sub>`` element
+-----------------
+
+- Child element of ``<rule>``.
+- Defines which glyph to replace when the rule evaluates to **True**.
+- The ``<sub>`` element contains a pair of glyphnames. The ``name`` attribute is the glyph that should be visible when the rule evaluates to **False**. The ``with`` attribute is the glyph that should be visible when the rule evaluates to **True**.
+
+
+.. rubric:: Attributes
+
+- ``name``: string, required. The name of the glyph this rule looks
+ for.
+- ``with``: string, required. The name of the glyph it is replaced
+ with.
+
+
+.. rubric:: Example
+
+Example with an implied ``<conditionset>``. Here the conditions are not
+contained in a conditionset.
+
+.. code:: xml
+
+ <rules processing="last">
+ <rule name="named.rule.1">
+ <condition minimum="250" maximum="750" name="weight" />
+ <condition minimum="50" maximum="100" name="width" />
+ <sub name="dollar" with="dollar.alt"/>
+ </rule>
+ </rules>
+
+Example with ``<conditionsets>``. All conditions in a conditionset must be true.
+
+.. code:: xml
+
+ <rules>
+ <rule name="named.rule.2">
+ <conditionset>
+ <condition minimum="250" maximum="750" name="weight" />
+ <condition minimum="50" maximum="100" name="width" />
+ </conditionset>
+ <conditionset>
+ <condition... />
+ <condition... />
+ </conditionset>
+ <sub name="dollar" with="dollar.alt"/>
+ </rule>
+ </rules>
+
+
+=====================
+``<sources>`` element
+=====================
+
+The ``<sources>`` element contains one or more ``<source>`` elements.
+
+
+``<source>`` element
+====================
+
+- Defines a single font or layer that contributes to the designspace.
+- Child element of ``<sources>``
+- Location in designspace coordinates.
+
+
+.. rubric:: Attributes
+
+- ``familyname``: optional, string. The family name of the source font.
+ While this could be extracted from the font data itself, it can be
+ more efficient to add it here.
+- ``stylename``: optional, string. The style name of the source font.
+- ``name``: required, string. A unique name that can be used to
+ identify this font if it needs to be referenced elsewhere.
+- ``filename``: required, string. A path to the source file, relative
+ to the root path of this document. The path can be at the same level
+ as the document or lower.
+- ``layer``: optional, string. The name of the layer in the source file.
+ If no layer attribute is given assume the foreground layer should be used.
+
+
+``<familyname>`` element: localised names for sources
+-----------------------------------------------------
+
+Localised family names for sources can be included with this ``<familyname>``
+element with an ``xml:lang`` attribute:
+`XML language definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
+
+.. versionadded:: 5.0
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <familyname xml:lang="fr">Montserrat</familyname>
+ <familyname xml:lang="ja">モンセラート</familyname>
+
+
+``<location>`` element (source)
+-------------------------------
+
+Defines the coordinates of this source in the design space.
+
+.. seealso:: `Full documentation of the <location> element <location>`__
+
+
+``<dimension>`` element (source)
+................................
+
+.. seealso:: `Full documentation of the <dimension> element <dimension>`__
+
+
+``<lib>`` element (source)
+--------------------------
+
+- Example: ``<lib copy="1" />``
+- Child element of ``<source>``
+- Defines if the instances can inherit the data in the lib of this source.
+- MutatorMath only.
+
+.. deprecated:: 5.0
+
+.. note::
+
+ Don't confuse with other ``<lib>`` elements which allow storing
+ arbitrary data. Sources don't have such a ``<lib>`` because usually the
+ backing UFO file has one itself.
+
+
+``<info>`` element
+------------------
+
+- Example: ``<info copy="1" />``
+- Child element of ``<source>``
+- Defines if the instances can inherit the non-interpolating font info
+ from this source.
+- MutatorMath only.
+
+.. deprecated:: 5.0
+
+
+``<features>`` element
+----------------------
+
+- Example: ``<features copy="1" />``
+- Defines if the instances can inherit opentype feature text from this
+ source.
+- Child element of ``<source>``
+- MutatorMath only.
+
+.. deprecated:: 5.0
+
+
+``<glyph>`` element (source)
+----------------------------
+
+- Example: ``<glyph mute="1" name="A"/>``
+- In a ``<source>`` element this states if a glyph is to be excluded from
+ the calculation.
+- MutatorMath only.
+
+.. rubric:: Attributes
+
+- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph
+ should be ignored as a master.
+
+.. note::
+
+ Do not confuse with the ``<glyph>`` element in instances, which achieves
+ something different.
+
+
+.. _kerning_source:
+
+``<kerning>`` element (source)
+------------------------------
+
+- Example: ``<kerning mute="1" />``
+- Can appear in ``<source>`` as well as in ``<instance>`` elements.
+- MutatorMath only.
+
+.. rubric:: Attributes
+
+- ``mute``: required attribute, number 1 or 0. Indicate if the kerning
+ data from this source is to be excluded from the calculation.
+
+ - If the kerning element is not present, assume ``mute=0``, yes,
+ include the kerning of this source in the calculation.
+
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <source familyname="MasterFamilyName" filename="masters/masterTest1.ufo" name="master.ufo1" stylename="MasterStyleNameOne">
+ <location>
+ <dimension name="width" xvalue="0.000000" />
+ <dimension name="weight" xvalue="0.000000" />
+ </location>
+ <glyph mute="1" name="A" />
+ <glyph mute="1" name="Z" />
+ </source>
+
+
+============================
+``<variable-fonts>`` element
+============================
+
+The ``<variable-fonts>`` element contains one or more ``<variable-font>`` elements.
+
+.. versionadded:: 5.0
+
+
+``<variable-font>`` element
+===========================
+
+- Child of ``<variable-fonts>``
+- Describe a variable font that can be built from an interpolating subset of
+ the design space.
+- The document may have zero to many variable fonts.
+
+ - If no variable fonts are defined, and all the axes are continuous, then we
+ assume, as in version 4 of the format, that the whole document describes
+ one variable font covering the whole space.
+
+- Each variable font covers a subset of the whole designspace, defined using
+ ``<axis-subset>`` elements.
+- Each variable font can have custom associated data using a ``<lib>`` element.
+
+.. versionadded:: 5.0
+
+.. rubric:: Attributes
+
+- ``name``: string, required. Each variable font has a name, that can be
+ used by build tools to refer to the font that gets built from this element.
+- ``filename``: string, optional. This filename will be used by tools to decide
+ where to store the built font on the disk. If not given, a filename can be
+ computed from the ``name``. The filename may include an extension (e.g.
+ `.ttf`) and the build tools can replace that extension with another (e.g.
+ `.otf` or `.woff2`) as needed.
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <variable-font name="MyFontVF_Italic">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Italic" uservalue="1"/>
+ </axis-subsets>
+ </variable-font>
+
+
+``<axis-subsets>`` element
+--------------------------
+
+- Child of ``<variable-font>``
+- Defines the portion of the design space that this variable font covers.
+- Each axis that you want to include in the VF needs to be mentioned here.
+- Not mentioning an axis is equivalent to slicing the space at the default
+ value of that axis.
+
+.. versionadded:: 5.0
+
+
+``<axis-subset>`` element
+.........................
+
+- Child of ``<axis-subsets>``
+- Defines the subset of one axis, by ``name=""``, that the variable font covers.
+- If this axis is continuous, the VF can either cover:
+
+ 1. the whole axis
+
+ .. code:: xml
+
+ <axis-subset name="Weight"/>
+
+ 2. a sub-range of the full axis
+
+ .. code:: xml
+
+ <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. *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
+
+ <!-- Make a bold VF -->
+ <axis-subset name="Weight" uservalue="700"/>
+
+- If this axis is discrete, then only the third option above is possible:
+ give one value along the axis.
+
+ .. code:: xml
+
+ <!-- Make an italic VF -->
+ <axis-subset name="Italic" uservalue="1"/>
+
+
+.. versionadded:: 5.0
+
+.. rubric:: Attributes
+
+- ``name``: required, string. Name of the axis to subset.
+
+When defining a range:
+
+- ``userminimum``: optional, number.
+ Lower end of the range, in user coordinates.
+ If not mentioned, assume the axis's minimum.
+- ``usermaximum``: optional, number.
+ Upper end of the range, in user coordinates.
+ If not mentioned, assume the axis's maximum.
+- ``userdefault``: optional, number.
+ New default value of subset axis, in user coordinates.
+ If not mentioned, assume the axis's default.
+ If the axis's default falls outside of the subset range, then the new default
+ will be the extremum that is closest to the full axis's default.
+
+When defining a single value:
+
+- ``uservalue``: required, number.
+ Single value, in user coordinates, at which to snapshot the design space
+ while building this VF.
+
+
+``<lib>`` element (variable font)
+---------------------------------
+
+Arbitrary data about this variable font.
+
+.. versionadded:: 5.0
+
+.. seealso:: :ref:`lib`
+
+
+Instances included in the variable font
+---------------------------------------
+
+.. figure:: v5_variable_fonts_vs_instances.png
+ :width: 650px
+ :alt: A designspace version 5 lists many instances and variable fonts. Each
+ variable font gets in its fvar table whichever instances fall within
+ the bounds of the variable font's subset axes.
+
+ Illustration of instances included in a variable font.
+
+
+=======================
+``<instances>`` element
+=======================
+
+The ``<instances>`` element contains one or more ``<instance>`` elements.
+
+
+``<instance>`` element
+======================
+
+- Defines a single font that can be calculated with the designspace.
+- Child element of ``<instances>``
+- For use in Varlib the instance element really only needs the names
+ and the location. The ``<glyphs>`` element is not required.
+- MutatorMath uses the ``<glyphs>`` element to describe how certain
+ glyphs need different masters, mainly to describe the effects of
+ conditional rules in Superpolator.
+- Location in designspace coordinates.
+
+
+.. rubric:: Attributes
+
+- ``familyname``: required, string. The family name of the instance
+ font. Corresponds with ``font.info.familyName``
+- ``stylename``: required, string. The style name of the instance font.
+ Corresponds with ``font.info.styleName``
+- ``name``: required, string. A unique name that can be used to
+ identify this font if it needs to be referenced elsewhere.
+- ``filename``: string. Required for MutatorMath. A path to the
+ instance file, relative to the root path of this document. The path
+ can be at the same level as the document or lower.
+- ``postscriptfontname``: string. Optional for MutatorMath. Corresponds
+ with ``font.info.postscriptFontName``
+- ``stylemapfamilyname``: string. Optional for MutatorMath. Corresponds
+ with ``styleMapFamilyName``
+- ``stylemapstylename``: string. Optional for MutatorMath. Corresponds
+ with ``styleMapStyleName``
+
+
+``<location>`` element (instance)
+---------------------------------
+
+Defines the coordinates of this instance in the design space.
+
+.. seealso:: `Full documentation of the <location> element <location>`__
+
+
+``<dimension>`` element (instance)
+..................................
+
+.. seealso:: `Full documentation of the <dimension> element <dimension>`__
+
+
+``<lib>`` element (instance)
+----------------------------
+
+Arbitrary data about this instance.
+
+.. seealso:: :ref:`lib`
+
+
+``<stylename>``, ``<familyname>``, ``<stylemapstylename>``, ``<stylemapfamilyname>`` elements: localised names for instances
+----------------------------------------------------------------------------------------------------------------------------
+
+Localised names for instances can be included with these simple elements
+with an ``xml:lang`` attribute:
+`XML language definition <https://www.w3.org/International/questions/qa-when-xmllang.en>`__
+
+- ``<stylename>``
+- ``<familyname>``
+- ``<stylemapstylename>``
+- ``<stylemapfamilyname>``
+
+
+.. rubric:: Example
+
+.. code:: xml
+
+ <stylename xml:lang="fr">Demigras</stylename>
+ <stylename xml:lang="ja">半ば</stylename>
+ <familyname xml:lang="fr">Montserrat</familyname>
+ <familyname xml:lang="ja">モンセラート</familyname>
+ <stylemapstylename xml:lang="de">Standard</stylemapstylename>
+ <stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
+ <stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
+
+
+Example for varlib
+------------------
+
+.. code:: xml
+
+ <instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
+ <location>
+ <dimension name="width" xvalue="400" yvalue="300" />
+ <dimension name="weight" xvalue="66" />
+ </location>
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.specimenText</key>
+ <string>Hamburgerwhatever</string>
+ </dict>
+ </lib>
+ </instance>
+
+
+``<glyphs>`` element (instance)
+-------------------------------
+
+- Container for ``<glyph>`` elements.
+- Optional
+- MutatorMath only.
+
+.. deprecated:: 5.0
+
+
+``<glyph>`` element (instance)
+..............................
+
+- Child element of ``<glyphs>``
+- May contain a ``<location>`` element.
+
+.. deprecated:: 5.0
+
+.. rubric:: Attributes
+
+- ``name``: string. The name of the glyph.
+- ``unicode``: string. Unicode values for this glyph, in hexadecimal.
+ Multiple values should be separated with a space.
+- ``mute``: optional attribute, number 1 or 0. Indicate if this glyph
+ should be supressed in the output.
+
+
+``<note>`` element
+,,,,,,,,,,,,,,,,,,
+
+- String. The value corresponds to glyph.note in UFO.
+
+.. deprecated:: 5.0
+
+
+``<masters>`` element
+,,,,,,,,,,,,,,,,,,,,,
+
+- Container for ``<master>`` elements
+- These ``<master>`` elements define an alternative set of glyph masters
+ for this glyph.
+
+.. deprecated:: 5.0
+
+
+``<master>`` element
+++++++++++++++++++++
+
+- Defines a single alternative master for this glyph.
+
+.. deprecated:: 5.0
+
+.. rubric:: Attributes
+
+- ``glyphname``: the name of the alternate master glyph.
+- ``source``: the identifier name of the source this master glyph needs
+ to be loaded from
+
+
+Example for MutatorMath
+.......................
+
+.. code:: xml
+
+ <instance familyname="InstanceFamilyName" filename="instances/instanceTest2.ufo" name="instance.ufo2" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName" stylename="InstanceStyleName">
+ <location>
+ <dimension name="width" xvalue="400" yvalue="300" />
+ <dimension name="weight" xvalue="66" />
+ </location>
+ <glyphs>
+ <glyph name="arrow2" />
+ <glyph name="arrow" unicode="0x4d2 0x4d3">
+ <location>
+ <dimension name="width" xvalue="100" />
+ <dimension name="weight" xvalue="120" />
+ </location>
+ <note>A note about this glyph</note>
+ <masters>
+ <master glyphname="BB" source="master.ufo1">
+ <location>
+ <dimension name="width" xvalue="20" />
+ <dimension name="weight" xvalue="20" />
+ </location>
+ </master>
+ </masters>
+ </glyph>
+ </glyphs>
+ <kerning />
+ <info />
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.specimenText</key>
+ <string>Hamburgerwhatever</string>
+ </dict>
+ </lib>
+ </instance>
+
+
+.. _lib:
+
+=============================
+``<lib>`` element (top-level)
+=============================
+
+The ``<lib>`` element contains arbitrary data.
+
+- Child element of ``<designspace>``, ``<variable-font>`` and ``<instance>``
+- Contains arbitrary data about the whole document or about a specific
+ variable font or instance.
+- Items in the dict need to use **reverse domain name notation**
+ <https://en.wikipedia.org/wiki/Reverse_domain_name_notation>__
+
+.. rubric:: Example:
+
+.. code:: xml
+
+ <lib>
+ <dict>
+ <key>com.company.fontEditor.myString</key>
+ <string>The contents use the PLIST format.</string>
+ </dict>
+ </lib>
+
+
diff --git a/Doc/source/index.rst b/Doc/source/index.rst
index 784834d8..571ef8dd 100644
--- a/Doc/source/index.rst
+++ b/Doc/source/index.rst
@@ -69,6 +69,7 @@ libraries in the fontTools suite:
- :py:mod:`fontTools.agl`: Access to the Adobe Glyph List
- :py:mod:`fontTools.cffLib`: Read/write tools for Adobe CFF fonts
- :py:mod:`fontTools.colorLib`: Module for handling colors in CPAL/COLR fonts
+- :py:mod:`fontTools.config`: Configure fontTools
- :py:mod:`fontTools.cu2qu`: Module for cubic to quadratic conversion
- :py:mod:`fontTools.designspaceLib`: Read and write designspace files
- :py:mod:`fontTools.encodings`: Support for font-related character encodings
@@ -120,6 +121,7 @@ Table of Contents
agl
cffLib/index
colorLib/index
+ config
cu2qu/index
designspaceLib/index
encodings/index
@@ -152,4 +154,4 @@ Table of Contents
:target: https://pypi.org/project/FontTools
.. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg
:alt: Join the chat at https://gitter.im/fonttools-dev/Lobby
- :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge \ No newline at end of file
+ :target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
diff --git a/Doc/source/misc/configTools.rst b/Doc/source/misc/configTools.rst
new file mode 100644
index 00000000..1d935411
--- /dev/null
+++ b/Doc/source/misc/configTools.rst
@@ -0,0 +1,8 @@
+###########
+configTools
+###########
+
+.. automodule:: fontTools.misc.configTools
+ :inherited-members:
+ :members:
+ :undoc-members:
diff --git a/Doc/source/misc/index.rst b/Doc/source/misc/index.rst
index 003c48a5..cfe52055 100644
--- a/Doc/source/misc/index.rst
+++ b/Doc/source/misc/index.rst
@@ -12,6 +12,7 @@ utilities by fontTools, but some of which may be more generally useful.
bezierTools
classifyTools
cliTools
+ configTools
eexec
encodingTools
etree
diff --git a/Lib/fontTools/Android.bp b/Lib/fontTools/Android.bp
index b7f6139c..8a0dfe08 100644
--- a/Lib/fontTools/Android.bp
+++ b/Lib/fontTools/Android.bp
@@ -31,12 +31,7 @@ package {
python_defaults {
name: "fonttools_default",
version: {
- py2: {
- enabled: false,
- embedded_launcher: false,
- },
py3: {
- enabled: true,
embedded_launcher: true,
},
},
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index 7fa7b304..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.31.2"
+version = __version__ = "4.37.1"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py
index 07d0d513..3eda9ba4 100644
--- a/Lib/fontTools/cffLib/__init__.py
+++ b/Lib/fontTools/cffLib/__init__.py
@@ -337,7 +337,7 @@ class CFFFontSet(object):
topDict = TopDict(
GlobalSubrs=self.GlobalSubrs,
cff2GetGlyphOrder=cff2GetGlyphOrder)
- self.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder, None)
+ self.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder)
self.topDictIndex.append(topDict)
for element in content:
if isinstance(element, str):
@@ -375,7 +375,7 @@ class CFFFontSet(object):
filled via :meth:`decompile`.)"""
self.major = 2
cff2GetGlyphOrder = self.otFont.getGlyphOrder
- topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
+ topDictData = TopDictIndex(None, cff2GetGlyphOrder)
topDictData.items = self.topDictIndex.items
self.topDictIndex = topDictData
topDict = topDictData[0]
@@ -1004,11 +1004,6 @@ class VarStoreData(object):
def decompile(self):
if self.file:
- class GlobalState(object):
- def __init__(self, tableType, cachingStats):
- self.tableType = tableType
- self.cachingStats = cachingStats
- globalState = GlobalState(tableType="VarStore", cachingStats={})
# read data in from file. Assume position is correct.
length = readCard16(self.file)
self.data = self.file.read(length)
@@ -1042,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/config/__init__.py b/Lib/fontTools/config/__init__.py
new file mode 100644
index 00000000..f5a62eaf
--- /dev/null
+++ b/Lib/fontTools/config/__init__.py
@@ -0,0 +1,59 @@
+"""
+Define all configuration options that can affect the working of fontTools
+modules. E.g. optimization levels of varLib IUP, otlLib GPOS compression level,
+etc. If this file gets too big, split it into smaller files per-module.
+
+An instance of the Config class can be attached to a TTFont object, so that
+the various modules can access their configuration options from it.
+"""
+from textwrap import dedent
+
+from fontTools.misc.configTools import *
+
+
+class Config(AbstractConfig):
+ options = Options()
+
+
+OPTIONS = Config.options
+
+
+Config.register_option(
+ name="fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
+ help=dedent(
+ """\
+ GPOS Lookup type 2 (PairPos) compression level:
+ 0 = do not attempt to compact PairPos lookups;
+ 1 to 8 = create at most 1 to 8 new subtables for each existing
+ subtable, provided that it would yield a 50%% file size saving;
+ 9 = create as many new subtables as needed to yield a file size saving.
+ Default: 0.
+
+ This compaction aims to save file size, by splitting large class
+ kerning subtables (Format 2) that contain many zero values into
+ smaller and denser subtables. It's a trade-off between the overhead
+ of several subtables versus the sparseness of one big subtable.
+
+ See the pull request: https://github.com/fonttools/fonttools/pull/2326
+ """
+ ),
+ default=0,
+ parse=int,
+ validate=lambda v: v in range(10),
+)
+
+Config.register_option(
+ name="fontTools.ttLib.tables.otBase:USE_HARFBUZZ_REPACKER",
+ help=dedent(
+ """\
+ FontTools tries to use the HarfBuzz Repacker to serialize GPOS/GSUB tables
+ if the uharfbuzz python bindings are importable, otherwise falls back to its
+ slower, less efficient serializer. Set to False to always use the latter.
+ Set to True to explicitly request the HarfBuzz Repacker (will raise an
+ error if uharfbuzz cannot be imported).
+ """
+ ),
+ default=None,
+ parse=Option.parse_optional_bool,
+ validate=Option.validate_optional_bool,
+)
diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py
index 4b706827..c74b5509 100644
--- a/Lib/fontTools/designspaceLib/__init__.py
+++ b/Lib/fontTools/designspaceLib/__init__.py
@@ -1,13 +1,19 @@
-# -*- coding: utf-8 -*-
+from __future__ import annotations
-from fontTools.misc.loggingTools import LogMixin
-from fontTools.misc.textTools import tobytes, tostr
import collections
-from io import BytesIO, StringIO
+import copy
+import itertools
+import math
import os
import posixpath
+from io import BytesIO, StringIO
+from textwrap import indent
+from typing import Any, Dict, List, MutableMapping, Optional, Tuple, Union, cast
+
from fontTools.misc import etree as ET
from fontTools.misc import plistlib
+from fontTools.misc.loggingTools import LogMixin
+from fontTools.misc.textTools import tobytes, tostr
"""
designSpaceDocument
@@ -16,9 +22,20 @@ from fontTools.misc import plistlib
"""
__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
@@ -40,6 +57,7 @@ def posix(path):
def posixpath_property(private_name):
+ """Generate a propery that holds a path always using forward slashes."""
def getter(self):
# Normal getter
return getattr(self, private_name)
@@ -93,16 +111,41 @@ class SimpleDescriptor(AsDictMixin):
except AssertionError:
print("failed attribute", attr, getattr(self, attr), "!=", getattr(other, attr))
+ def __repr__(self):
+ attrs = [f"{a}={repr(getattr(self, a))}," for a in self._attrs]
+ attrs = indent('\n'.join(attrs), ' ')
+ return f"{self.__class__.__name__}(\n{attrs}\n)"
+
class SourceDescriptor(SimpleDescriptor):
- """Simple container for data related to the source"""
+ """Simple container for data related to the source
+
+ .. code:: python
+
+ doc = DesignSpaceDocument()
+ s1 = SourceDescriptor()
+ s1.path = masterPath1
+ s1.name = "master.ufo1"
+ s1.font = defcon.Font("master.ufo1")
+ s1.location = dict(weight=0)
+ s1.familyName = "MasterFamilyName"
+ s1.styleName = "MasterStyleNameOne"
+ s1.localisedFamilyName = dict(fr="Caractère")
+ s1.mutedGlyphNames.append("A")
+ s1.mutedGlyphNames.append("Z")
+ doc.addSource(s1)
+
+ """
flavor = "source"
_attrs = ['filename', 'path', 'name', 'layerName',
'location', 'copyLib',
'copyGroups', 'copyFeatures',
'muteKerning', 'muteInfo',
'mutedGlyphNames',
- 'familyName', 'styleName']
+ 'familyName', 'styleName', 'localisedFamilyName']
+
+ filename = posixpath_property("_filename")
+ path = posixpath_property("_path")
def __init__(
self,
@@ -112,9 +155,11 @@ class SourceDescriptor(SimpleDescriptor):
font=None,
name=None,
location=None,
+ designLocation=None,
layerName=None,
familyName=None,
styleName=None,
+ localisedFamilyName=None,
copyLib=False,
copyInfo=False,
copyGroups=False,
@@ -124,8 +169,10 @@ class SourceDescriptor(SimpleDescriptor):
mutedGlyphNames=None,
):
self.filename = filename
- """The original path as found in the document."""
+ """string. A relative path to the source file, **as it is in the document**.
+ MutatorMath + VarLib.
+ """
self.path = path
"""The absolute path, calculated from filename."""
@@ -142,27 +189,158 @@ class SourceDescriptor(SimpleDescriptor):
"""
self.name = name
- self.location = location
+ """string. Optional. Unique identifier name for this source.
+
+ MutatorMath + Varlib.
+ """
+
+ self.designLocation = designLocation if designLocation is not None else location or {}
+ """dict. Axis values for this source, in design space coordinates.
+
+ MutatorMath + Varlib.
+
+ This may be only part of the full design location.
+ See :meth:`getFullDesignLocation()`
+
+ .. versionadded:: 5.0
+ """
+
self.layerName = layerName
+ """string. The name of the layer in the source to look for
+ outline data. Default ``None`` which means ``foreground``.
+ """
self.familyName = familyName
+ """string. Family name of this source. Though this data
+ can be extracted from the font, it can be efficient to have it right
+ here.
+
+ Varlib.
+ """
self.styleName = styleName
+ """string. Style name of this source. Though this data
+ can be extracted from the font, it can be efficient to have it right
+ here.
+
+ Varlib.
+ """
+ self.localisedFamilyName = localisedFamilyName or {}
+ """dict. A dictionary of localised family name strings, keyed by
+ language code.
+
+ If present, will be used to build localized names for all instances.
+
+ .. versionadded:: 5.0
+ """
self.copyLib = copyLib
+ """bool. Indicates if the contents of the font.lib need to
+ be copied to the instances.
+
+ MutatorMath.
+
+ .. deprecated:: 5.0
+ """
self.copyInfo = copyInfo
+ """bool. Indicates if the non-interpolating font.info needs
+ to be copied to the instances.
+
+ MutatorMath.
+
+ .. deprecated:: 5.0
+ """
self.copyGroups = copyGroups
+ """bool. Indicates if the groups need to be copied to the
+ instances.
+
+ MutatorMath.
+
+ .. deprecated:: 5.0
+ """
self.copyFeatures = copyFeatures
+ """bool. Indicates if the feature text needs to be
+ copied to the instances.
+
+ MutatorMath.
+
+ .. deprecated:: 5.0
+ """
self.muteKerning = muteKerning
+ """bool. Indicates if the kerning data from this source
+ needs to be muted (i.e. not be part of the calculations).
+
+ MutatorMath only.
+ """
self.muteInfo = muteInfo
+ """bool. Indicated if the interpolating font.info data for
+ this source needs to be muted.
+
+ MutatorMath only.
+ """
self.mutedGlyphNames = mutedGlyphNames or []
+ """list. Glyphnames that need to be muted in the
+ instances.
+
+ MutatorMath only.
+ """
+
+ @property
+ def location(self):
+ """dict. Axis values for this source, in design space coordinates.
+
+ MutatorMath + Varlib.
+
+ .. deprecated:: 5.0
+ Use the more explicit alias for this property :attr:`designLocation`.
+ """
+ return self.designLocation
+
+ @location.setter
+ def location(self, location: Optional[AnisotropicLocationDict]):
+ self.designLocation = location or {}
+
+ def setFamilyName(self, familyName, languageCode="en"):
+ """Setter for :attr:`localisedFamilyName`
+
+ .. versionadded:: 5.0
+ """
+ self.localisedFamilyName[languageCode] = tostr(familyName)
+
+ def getFamilyName(self, languageCode="en"):
+ """Getter for :attr:`localisedFamilyName`
+
+ .. versionadded:: 5.0
+ """
+ return self.localisedFamilyName.get(languageCode)
- path = posixpath_property("_path")
- filename = posixpath_property("_filename")
+
+ def getFullDesignLocation(self, doc: 'DesignSpaceDocument') -> AnisotropicLocationDict:
+ """Get the complete design location of this source, from its
+ :attr:`designLocation` and the document's axis defaults.
+
+ .. versionadded:: 5.0
+ """
+ result: AnisotropicLocationDict = {}
+ for axis in doc.axes:
+ if axis.name in self.designLocation:
+ result[axis.name] = self.designLocation[axis.name]
+ else:
+ result[axis.name] = axis.map_forward(axis.default)
+ return result
class RuleDescriptor(SimpleDescriptor):
- """Represents the rule descriptor element
+ """Represents the rule descriptor element: a set of glyph substitutions to
+ trigger conditionally in some parts of the designspace.
- .. code-block:: xml
+ .. code:: python
+
+ r1 = RuleDescriptor()
+ r1.name = "unique.rule.name"
+ r1.conditionSets.append([dict(name="weight", minimum=-10, maximum=10), dict(...)])
+ r1.conditionSets.append([dict(...), dict(...)])
+ r1.subs.append(("a", "a.alt"))
+
+ .. code:: xml
<!-- optional: list of substitution rules -->
<rules>
@@ -181,21 +359,36 @@ class RuleDescriptor(SimpleDescriptor):
def __init__(self, *, name=None, conditionSets=None, subs=None):
self.name = name
+ """string. Unique name for this rule. Can be used to reference this rule data."""
# list of lists of dict(name='aaaa', minimum=0, maximum=1000)
self.conditionSets = conditionSets or []
+ """a list of conditionsets.
+
+ - Each conditionset is a list of conditions.
+ - Each condition is a dict with ``name``, ``minimum`` and ``maximum`` keys.
+ """
# list of substitutions stored as tuples of glyphnames ("a", "a.alt")
self.subs = subs or []
+ """list of substitutions.
+
+ - Each substitution is stored as tuples of glyphnames, e.g. ("a", "a.alt").
+ - Note: By default, rules are applied first, before other text
+ shaping/OpenType layout, as they are part of the
+ `Required Variation Alternates OpenType feature <https://docs.microsoft.com/en-us/typography/opentype/spec/features_pt#-tag-rvrn>`_.
+ See ref:`rules-element` § Attributes.
+ """
def evaluateRule(rule, location):
- """ Return True if any of the rule's conditionsets matches the given location."""
+ """Return True if any of the rule's conditionsets matches the given location."""
return any(evaluateConditions(c, location) for c in rule.conditionSets)
def evaluateConditions(conditions, location):
- """ Return True if all the conditions matches the given location.
- If a condition has no minimum, check for < maximum.
- If a condition has no maximum, check for > minimum.
+ """Return True if all the conditions matches the given location.
+
+ - If a condition has no minimum, check for < maximum.
+ - If a condition has no maximum, check for > minimum.
"""
for cd in conditions:
value = location[cd['name']]
@@ -211,8 +404,11 @@ def evaluateConditions(conditions, location):
def processRules(rules, location, glyphNames):
- """ Apply these rules at this location to these glyphnames
- - rule order matters
+ """Apply these rules at this location to these glyphnames.
+
+ Return a new list of glyphNames with substitutions applied.
+
+ - rule order matters
"""
newNames = []
for rule in rules:
@@ -232,22 +428,54 @@ def processRules(rules, location, glyphNames):
return glyphNames
+AnisotropicLocationDict = Dict[str, Union[float, Tuple[float, float]]]
+SimpleLocationDict = Dict[str, float]
+
+
class InstanceDescriptor(SimpleDescriptor):
- """Simple container for data related to the instance"""
+ """Simple container for data related to the instance
+
+
+ .. code:: python
+
+ i2 = InstanceDescriptor()
+ i2.path = instancePath2
+ i2.familyName = "InstanceFamilyName"
+ i2.styleName = "InstanceStyleName"
+ i2.name = "instance.ufo2"
+ # anisotropic location
+ i2.designLocation = dict(weight=500, width=(400,300))
+ i2.postScriptFontName = "InstancePostscriptName"
+ i2.styleMapFamilyName = "InstanceStyleMapFamilyName"
+ i2.styleMapStyleName = "InstanceStyleMapStyleName"
+ i2.lib['com.coolDesignspaceApp.specimenText'] = 'Hamburgerwhatever'
+ doc.addInstance(i2)
+ """
flavor = "instance"
_defaultLanguageCode = "en"
- _attrs = ['path',
+ _attrs = ['filename',
+ 'path',
'name',
- 'location',
+ 'locationLabel',
+ 'designLocation',
+ 'userLocation',
'familyName',
'styleName',
'postScriptFontName',
'styleMapFamilyName',
'styleMapStyleName',
+ 'localisedFamilyName',
+ 'localisedStyleName',
+ 'localisedStyleMapFamilyName',
+ 'localisedStyleMapStyleName',
+ 'glyphs',
'kerning',
'info',
'lib']
+ filename = posixpath_property("_filename")
+ path = posixpath_property("_path")
+
def __init__(
self,
*,
@@ -256,6 +484,9 @@ class InstanceDescriptor(SimpleDescriptor):
font=None,
name=None,
location=None,
+ locationLabel=None,
+ designLocation=None,
+ userLocation=None,
familyName=None,
styleName=None,
postScriptFontName=None,
@@ -270,34 +501,148 @@ class InstanceDescriptor(SimpleDescriptor):
info=True,
lib=None,
):
- # the original path as found in the document
self.filename = filename
- # the absolute path, calculated from filename
+ """string. Relative path to the instance file, **as it is
+ in the document**. The file may or may not exist.
+
+ MutatorMath + VarLib.
+ """
self.path = path
- # Same as in SourceDescriptor.
+ """string. Absolute path to the instance file, calculated from
+ the document path and the string in the filename attr. The file may
+ or may not exist.
+
+ MutatorMath.
+ """
self.font = font
+ """Same as :attr:`SourceDescriptor.font`
+
+ .. seealso:: :attr:`SourceDescriptor.font`
+ """
self.name = name
- self.location = location
+ """string. Unique identifier name of the instance, used to
+ identify it if it needs to be referenced from elsewhere in the
+ document.
+ """
+ self.locationLabel = locationLabel
+ """Name of a :class:`LocationLabelDescriptor`. If
+ provided, the instance should have the same location as the
+ LocationLabel.
+
+ .. seealso::
+ :meth:`getFullDesignLocation`
+ :meth:`getFullUserLocation`
+
+ .. versionadded:: 5.0
+ """
+ self.designLocation: AnisotropicLocationDict = designLocation if designLocation is not None else (location or {})
+ """dict. Axis values for this instance, in design space coordinates.
+
+ MutatorMath + Varlib.
+
+ .. seealso:: This may be only part of the full location. See:
+ :meth:`getFullDesignLocation`
+ :meth:`getFullUserLocation`
+
+ .. versionadded:: 5.0
+ """
+ self.userLocation: SimpleLocationDict = userLocation or {}
+ """dict. Axis values for this instance, in user space coordinates.
+
+ MutatorMath + Varlib.
+
+ .. seealso:: This may be only part of the full location. See:
+ :meth:`getFullDesignLocation`
+ :meth:`getFullUserLocation`
+
+ .. versionadded:: 5.0
+ """
self.familyName = familyName
+ """string. Family name of this instance.
+
+ MutatorMath + Varlib.
+ """
self.styleName = styleName
+ """string. Style name of this instance.
+
+ MutatorMath + Varlib.
+ """
self.postScriptFontName = postScriptFontName
+ """string. Postscript fontname for this instance.
+
+ MutatorMath + Varlib.
+ """
self.styleMapFamilyName = styleMapFamilyName
+ """string. StyleMap familyname for this instance.
+
+ MutatorMath + Varlib.
+ """
self.styleMapStyleName = styleMapStyleName
+ """string. StyleMap stylename for this instance.
+
+ MutatorMath + Varlib.
+ """
self.localisedFamilyName = localisedFamilyName or {}
+ """dict. A dictionary of localised family name
+ strings, keyed by language code.
+ """
self.localisedStyleName = localisedStyleName or {}
+ """dict. A dictionary of localised stylename
+ strings, keyed by language code.
+ """
self.localisedStyleMapFamilyName = localisedStyleMapFamilyName or {}
+ """A dictionary of localised style map
+ familyname strings, keyed by language code.
+ """
self.localisedStyleMapStyleName = localisedStyleMapStyleName or {}
+ """A dictionary of localised style map
+ stylename strings, keyed by language code.
+ """
self.glyphs = glyphs or {}
+ """dict for special master definitions for glyphs. If glyphs
+ need special masters (to record the results of executed rules for
+ example).
+
+ MutatorMath.
+
+ .. deprecated:: 5.0
+ Use rules or sparse sources instead.
+ """
self.kerning = kerning
+ """ bool. Indicates if this instance needs its kerning
+ calculated.
+
+ MutatorMath.
+
+ .. deprecated:: 5.0
+ """
self.info = info
+ """bool. Indicated if this instance needs the interpolating
+ font.info calculated.
+
+ .. deprecated:: 5.0
+ """
self.lib = lib or {}
"""Custom data associated with this instance."""
- path = posixpath_property("_path")
- filename = posixpath_property("_filename")
+ @property
+ def location(self):
+ """dict. Axis values for this instance.
+
+ MutatorMath + Varlib.
+
+ .. deprecated:: 5.0
+ Use the more explicit alias for this property :attr:`designLocation`.
+ """
+ return self.designLocation
+
+ @location.setter
+ def location(self, location: Optional[AnisotropicLocationDict]):
+ self.designLocation = location or {}
def setStyleName(self, styleName, languageCode="en"):
+ """These methods give easier access to the localised names."""
self.localisedStyleName[languageCode] = tostr(styleName)
def getStyleName(self, languageCode="en"):
@@ -321,6 +666,106 @@ class InstanceDescriptor(SimpleDescriptor):
def getStyleMapFamilyName(self, languageCode="en"):
return self.localisedStyleMapFamilyName.get(languageCode)
+ def clearLocation(self, axisName: Optional[str] = None):
+ """Clear all location-related fields. Ensures that
+ :attr:``designLocation`` and :attr:``userLocation`` are dictionaries
+ (possibly empty if clearing everything).
+
+ In order to update the location of this instance wholesale, a user
+ should first clear all the fields, then change the field(s) for which
+ they have data.
+
+ .. code:: python
+
+ instance.clearLocation()
+ instance.designLocation = {'Weight': (34, 36.5), 'Width': 100}
+ instance.userLocation = {'Opsz': 16}
+
+ In order to update a single axis location, the user should only clear
+ that axis, then edit the values:
+
+ .. code:: python
+
+ instance.clearLocation('Weight')
+ instance.designLocation['Weight'] = (34, 36.5)
+
+ Args:
+ axisName: if provided, only clear the location for that axis.
+
+ .. versionadded:: 5.0
+ """
+ self.locationLabel = None
+ if axisName is None:
+ self.designLocation = {}
+ self.userLocation = {}
+ else:
+ if self.designLocation is None:
+ self.designLocation = {}
+ if axisName in self.designLocation:
+ del self.designLocation[axisName]
+ if self.userLocation is None:
+ self.userLocation = {}
+ if axisName in self.userLocation:
+ del self.userLocation[axisName]
+
+ def getLocationLabelDescriptor(self, doc: 'DesignSpaceDocument') -> Optional[LocationLabelDescriptor]:
+ """Get the :class:`LocationLabelDescriptor` instance that matches
+ this instances's :attr:`locationLabel`.
+
+ Raises if the named label can't be found.
+
+ .. versionadded:: 5.0
+ """
+ if self.locationLabel is None:
+ return None
+ label = doc.getLocationLabel(self.locationLabel)
+ if label is None:
+ raise DesignSpaceDocumentError(
+ 'InstanceDescriptor.getLocationLabelDescriptor(): '
+ f'unknown location label `{self.locationLabel}` in instance `{self.name}`.'
+ )
+ return label
+
+ def getFullDesignLocation(self, doc: 'DesignSpaceDocument') -> AnisotropicLocationDict:
+ """Get the complete design location of this instance, by combining data
+ from the various location fields, default axis values and mappings, and
+ top-level location labels.
+
+ The source of truth for this instance's location is determined for each
+ axis independently by taking the first not-None field in this list:
+
+ - ``locationLabel``: the location along this axis is the same as the
+ matching STAT format 4 label. No anisotropy.
+ - ``designLocation[axisName]``: the explicit design location along this
+ axis, possibly anisotropic.
+ - ``userLocation[axisName]``: the explicit user location along this
+ axis. No anisotropy.
+ - ``axis.default``: default axis value. No anisotropy.
+
+ .. versionadded:: 5.0
+ """
+ label = self.getLocationLabelDescriptor(doc)
+ if label is not None:
+ return doc.map_forward(label.userLocation) # type: ignore
+ result: AnisotropicLocationDict = {}
+ for axis in doc.axes:
+ if axis.name in self.designLocation:
+ result[axis.name] = self.designLocation[axis.name]
+ elif axis.name in self.userLocation:
+ result[axis.name] = axis.map_forward(self.userLocation[axis.name])
+ else:
+ result[axis.name] = axis.map_forward(axis.default)
+ return result
+
+ def getFullUserLocation(self, doc: 'DesignSpaceDocument') -> SimpleLocationDict:
+ """Get the complete user location for this instance.
+
+ .. seealso:: :meth:`getFullDesignLocation`
+
+ .. versionadded:: 5.0
+ """
+ return doc.map_backward(self.getFullDesignLocation(doc))
+
def tagForAxisName(name):
# try to find or make a tag name for this axis name
@@ -340,12 +785,8 @@ def tagForAxisName(name):
return tag, dict(en=name)
-class AxisDescriptor(SimpleDescriptor):
- """ Simple container for the axis data
- Add more localisations?
- """
+class AbstractAxisDescriptor(SimpleDescriptor):
flavor = "axis"
- _attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map']
def __init__(
self,
@@ -353,23 +794,122 @@ class AxisDescriptor(SimpleDescriptor):
tag=None,
name=None,
labelNames=None,
- minimum=None,
- default=None,
- maximum=None,
hidden=False,
map=None,
+ axisOrdering=None,
+ axisLabels=None,
):
# opentype tag for this axis
self.tag = tag
+ """string. Four letter tag for this axis. Some might be
+ registered at the `OpenType
+ specification <https://www.microsoft.com/typography/otspec/fvar.htm#VAT>`__.
+ Privately-defined axis tags must begin with an uppercase letter and
+ use only uppercase letters or digits.
+ """
# name of the axis used in locations
self.name = name
+ """string. Name of the axis as it is used in the location dicts.
+
+ MutatorMath + Varlib.
+ """
# names for UI purposes, if this is not a standard axis,
self.labelNames = labelNames or {}
+ """dict. When defining a non-registered axis, it will be
+ necessary to define user-facing readable names for the axis. Keyed by
+ xml:lang code. Values are required to be ``unicode`` strings, even if
+ they only contain ASCII characters.
+ """
+ self.hidden = hidden
+ """bool. Whether this axis should be hidden in user interfaces.
+ """
+ self.map = map or []
+ """list of input / output values that can describe a warp of user space
+ to design space coordinates. If no map values are present, it is assumed
+ user space is the same as design space, as in [(minimum, minimum),
+ (maximum, maximum)].
+
+ Varlib.
+ """
+ self.axisOrdering = axisOrdering
+ """STAT table field ``axisOrdering``.
+
+ See: `OTSpec STAT Axis Record <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-records>`_
+
+ .. versionadded:: 5.0
+ """
+ self.axisLabels: List[AxisLabelDescriptor] = axisLabels or []
+ """STAT table entries for Axis Value Tables format 1, 2, 3.
+
+ See: `OTSpec STAT Axis Value Tables <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-tables>`_
+
+ .. versionadded:: 5.0
+ """
+
+
+class AxisDescriptor(AbstractAxisDescriptor):
+ """ Simple container for the axis data.
+
+ Add more localisations?
+
+ .. code:: python
+
+ a1 = AxisDescriptor()
+ a1.minimum = 1
+ a1.maximum = 1000
+ a1.default = 400
+ a1.name = "weight"
+ a1.tag = "wght"
+ a1.labelNames['fa-IR'] = "قطر"
+ a1.labelNames['en'] = "Wéíght"
+ a1.map = [(1.0, 10.0), (400.0, 66.0), (1000.0, 990.0)]
+ a1.axisOrdering = 1
+ a1.axisLabels = [
+ AxisLabelDescriptor(name="Regular", userValue=400, elidable=True)
+ ]
+ doc.addAxis(a1)
+ """
+ _attrs = ['tag', 'name', 'maximum', 'minimum', 'default', 'map', 'axisOrdering', 'axisLabels']
+
+ def __init__(
+ self,
+ *,
+ tag=None,
+ name=None,
+ labelNames=None,
+ minimum=None,
+ default=None,
+ maximum=None,
+ hidden=False,
+ map=None,
+ axisOrdering=None,
+ axisLabels=None,
+ ):
+ super().__init__(
+ tag=tag,
+ name=name,
+ labelNames=labelNames,
+ hidden=hidden,
+ map=map,
+ axisOrdering=axisOrdering,
+ axisLabels=axisLabels,
+ )
self.minimum = minimum
+ """number. The minimum value for this axis in user space.
+
+ MutatorMath + Varlib.
+ """
self.maximum = maximum
+ """number. The maximum value for this axis in user space.
+
+ MutatorMath + Varlib.
+ """
self.default = default
- self.hidden = hidden
- self.map = map or []
+ """number. The default value for this axis, i.e. when a new location is
+ created, this is the value this axis will get in user space.
+
+ MutatorMath + Varlib.
+ """
def serialize(self):
# output to a dict, used in testing
@@ -382,9 +922,12 @@ class AxisDescriptor(SimpleDescriptor):
default=self.default,
hidden=self.hidden,
map=self.map,
+ axisOrdering=self.axisOrdering,
+ axisLabels=self.axisLabels,
)
def map_forward(self, v):
+ """Maps value from axis mapping's input (user) to output (design)."""
from fontTools.varLib.models import piecewiseLinearMap
if not self.map:
@@ -392,18 +935,350 @@ class AxisDescriptor(SimpleDescriptor):
return piecewiseLinearMap(v, {k: v for k, v in self.map})
def map_backward(self, v):
+ """Maps value from axis mapping's output (design) to input (user)."""
from fontTools.varLib.models import piecewiseLinearMap
+ if isinstance(v, tuple):
+ v = v[0]
if not self.map:
return v
return piecewiseLinearMap(v, {v: k for k, v in self.map})
+class DiscreteAxisDescriptor(AbstractAxisDescriptor):
+ """Container for discrete axis data.
+
+ Use this for axes that do not interpolate. The main difference from a
+ continuous axis is that a continuous axis has a ``minimum`` and ``maximum``,
+ while a discrete axis has a list of ``values``.
+
+ Example: an Italic axis with 2 stops, Roman and Italic, that are not
+ compatible. The axis still allows to bind together the full font family,
+ which is useful for the STAT table, however it can't become a variation
+ axis in a VF.
+
+ .. code:: python
+
+ a2 = DiscreteAxisDescriptor()
+ a2.values = [0, 1]
+ a2.default = 0
+ a2.name = "Italic"
+ a2.tag = "ITAL"
+ a2.labelNames['fr'] = "Italique"
+ a2.map = [(0, 0), (1, -11)]
+ a2.axisOrdering = 2
+ a2.axisLabels = [
+ AxisLabelDescriptor(name="Roman", userValue=0, elidable=True)
+ ]
+ doc.addAxis(a2)
+
+ .. versionadded:: 5.0
+ """
+
+ flavor = "axis"
+ _attrs = ('tag', 'name', 'values', 'default', 'map', 'axisOrdering', 'axisLabels')
+
+ def __init__(
+ self,
+ *,
+ tag=None,
+ name=None,
+ labelNames=None,
+ values=None,
+ default=None,
+ hidden=False,
+ map=None,
+ axisOrdering=None,
+ axisLabels=None,
+ ):
+ super().__init__(
+ tag=tag,
+ name=name,
+ labelNames=labelNames,
+ hidden=hidden,
+ map=map,
+ axisOrdering=axisOrdering,
+ axisLabels=axisLabels,
+ )
+ self.default: float = default
+ """The default value for this axis, i.e. when a new location is
+ created, this is the value this axis will get in user space.
+
+ However, this default value is less important than in continuous axes:
+
+ - it doesn't define the "neutral" version of outlines from which
+ deltas would apply, as this axis does not interpolate.
+ - it doesn't provide the reference glyph set for the designspace, as
+ fonts at each value can have different glyph sets.
+ """
+ self.values: List[float] = values or []
+ """List of possible values for this axis. Contrary to continuous axes,
+ only the values in this list can be taken by the axis, nothing in-between.
+ """
+
+ def map_forward(self, value):
+ """Maps value from axis mapping's input to output.
+
+ Returns value unchanged if no mapping entry is found.
+
+ Note: for discrete axes, each value must have its mapping entry, if
+ you intend that value to be mapped.
+ """
+ return next((v for k, v in self.map if k == value), value)
+
+ def map_backward(self, value):
+ """Maps value from axis mapping's output to input.
+
+ Returns value unchanged if no mapping entry is found.
+
+ Note: for discrete axes, each value must have its mapping entry, if
+ you intend that value to be mapped.
+ """
+ if isinstance(value, tuple):
+ value = value[0]
+ return next((k for k, v in self.map if v == value), value)
+
+
+class AxisLabelDescriptor(SimpleDescriptor):
+ """Container for axis label data.
+
+ Analogue of OpenType's STAT data for a single axis (formats 1, 2 and 3).
+ All values are user values.
+ See: `OTSpec STAT Axis value table, format 1, 2, 3 <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-1>`_
+
+ The STAT format of the Axis value depends on which field are filled-in,
+ see :meth:`getFormat`
+
+ .. versionadded:: 5.0
+ """
+
+ flavor = "label"
+ _attrs = ('userMinimum', 'userValue', 'userMaximum', 'name', 'elidable', 'olderSibling', 'linkedUserValue', 'labelNames')
+
+ def __init__(
+ self,
+ *,
+ name,
+ userValue,
+ userMinimum=None,
+ userMaximum=None,
+ elidable=False,
+ olderSibling=False,
+ linkedUserValue=None,
+ labelNames=None,
+ ):
+ self.userMinimum: Optional[float] = userMinimum
+ """STAT field ``rangeMinValue`` (format 2)."""
+ self.userValue: float = userValue
+ """STAT field ``value`` (format 1, 3) or ``nominalValue`` (format 2)."""
+ self.userMaximum: Optional[float] = userMaximum
+ """STAT field ``rangeMaxValue`` (format 2)."""
+ self.name: str = name
+ """Label for this axis location, STAT field ``valueNameID``."""
+ self.elidable: bool = elidable
+ """STAT flag ``ELIDABLE_AXIS_VALUE_NAME``.
+
+ See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
+ """
+ self.olderSibling: bool = olderSibling
+ """STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``.
+
+ See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
+ """
+ self.linkedUserValue: Optional[float] = linkedUserValue
+ """STAT field ``linkedValue`` (format 3)."""
+ self.labelNames: MutableMapping[str, str] = labelNames or {}
+ """User-facing translations of this location's label. Keyed by
+ ``xml:lang`` code.
+ """
+
+ def getFormat(self) -> int:
+ """Determine which format of STAT Axis value to use to encode this label.
+
+ =========== ========= =========== =========== ===============
+ STAT Format userValue userMinimum userMaximum linkedUserValue
+ =========== ========= =========== =========== ===============
+ 1 ✅ ❌ ❌ ❌
+ 2 ✅ ✅ ✅ ❌
+ 3 ✅ ❌ ❌ ✅
+ =========== ========= =========== =========== ===============
+ """
+ if self.linkedUserValue is not None:
+ return 3
+ if self.userMinimum is not None or self.userMaximum is not None:
+ return 2
+ return 1
+
+ @property
+ def defaultName(self) -> str:
+ """Return the English name from :attr:`labelNames` or the :attr:`name`."""
+ return self.labelNames.get("en") or self.name
+
+
+class LocationLabelDescriptor(SimpleDescriptor):
+ """Container for location label data.
+
+ Analogue of OpenType's STAT data for a free-floating location (format 4).
+ All values are user values.
+
+ See: `OTSpec STAT Axis value table, format 4 <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4>`_
+
+ .. versionadded:: 5.0
+ """
+
+ flavor = "label"
+ _attrs = ('name', 'elidable', 'olderSibling', 'userLocation', 'labelNames')
+
+ def __init__(
+ self,
+ *,
+ name,
+ userLocation,
+ elidable=False,
+ olderSibling=False,
+ labelNames=None,
+ ):
+ self.name: str = name
+ """Label for this named location, STAT field ``valueNameID``."""
+ self.userLocation: SimpleLocationDict = userLocation or {}
+ """Location in user coordinates along each axis.
+
+ If an axis is not mentioned, it is assumed to be at its default location.
+
+ .. seealso:: This may be only part of the full location. See:
+ :meth:`getFullUserLocation`
+ """
+ self.elidable: bool = elidable
+ """STAT flag ``ELIDABLE_AXIS_VALUE_NAME``.
+
+ See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
+ """
+ self.olderSibling: bool = olderSibling
+ """STAT flag ``OLDER_SIBLING_FONT_ATTRIBUTE``.
+
+ See: `OTSpec STAT Flags <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#flags>`_
+ """
+ self.labelNames: Dict[str, str] = labelNames or {}
+ """User-facing translations of this location's label. Keyed by
+ xml:lang code.
+ """
+
+ @property
+ def defaultName(self) -> str:
+ """Return the English name from :attr:`labelNames` or the :attr:`name`."""
+ return self.labelNames.get("en") or self.name
+
+ def getFullUserLocation(self, doc: 'DesignSpaceDocument') -> SimpleLocationDict:
+ """Get the complete user location of this label, by combining data
+ from the explicit user location and default axis values.
+
+ .. versionadded:: 5.0
+ """
+ return {
+ axis.name: self.userLocation.get(axis.name, axis.default)
+ for axis in doc.axes
+ }
+
+
+class VariableFontDescriptor(SimpleDescriptor):
+ """Container for variable fonts, sub-spaces of the Designspace.
+
+ Use-cases:
+
+ - From a single DesignSpace with discrete axes, define 1 variable font
+ per value on the discrete axes. Before version 5, you would have needed
+ 1 DesignSpace per such variable font, and a lot of data duplication.
+ - From a big variable font with many axes, define subsets of that variable
+ font that only include some axes and freeze other axes at a given location.
+
+ .. versionadded:: 5.0
+ """
+
+ flavor = "variable-font"
+ _attrs = ('filename', 'axisSubsets', 'lib')
+
+ filename = posixpath_property("_filename")
+
+ def __init__(self, *, name, filename=None, axisSubsets=None, lib=None):
+ self.name: str = name
+ """string, required. Name of this variable to identify it during the
+ build process and from other parts of the document, and also as a
+ filename in case the filename property is empty.
+
+ VarLib.
+ """
+ self.filename: str = filename
+ """string, optional. Relative path to the variable font file, **as it is
+ in the document**. The file may or may not exist.
+
+ If not specified, the :attr:`name` will be used as a basename for the file.
+ """
+ self.axisSubsets: List[Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor]] = axisSubsets or []
+ """Axis subsets to include in this variable font.
+
+ If an axis is not mentioned, assume that we only want the default
+ location of that axis (same as a :class:`ValueAxisSubsetDescriptor`).
+ """
+ self.lib: MutableMapping[str, Any] = lib or {}
+ """Custom data associated with this variable font."""
+
+
+class RangeAxisSubsetDescriptor(SimpleDescriptor):
+ """Subset of a continuous axis to include in a variable font.
+
+ .. versionadded:: 5.0
+ """
+ flavor = "axis-subset"
+ _attrs = ('name', 'userMinimum', 'userDefault', 'userMaximum')
+
+ def __init__(self, *, name, userMinimum=-math.inf, userDefault=None, userMaximum=math.inf):
+ self.name: str = name
+ """Name of the :class:`AxisDescriptor` to subset."""
+ self.userMinimum: float = userMinimum
+ """New minimum value of the axis in the target variable font.
+ If not specified, assume the same minimum value as the full axis.
+ (default = ``-math.inf``)
+ """
+ self.userDefault: Optional[float] = userDefault
+ """New default value of the axis in the target variable font.
+ If not specified, assume the same default value as the full axis.
+ (default = ``None``)
+ """
+ self.userMaximum: float = userMaximum
+ """New maximum value of the axis in the target variable font.
+ If not specified, assume the same maximum value as the full axis.
+ (default = ``math.inf``)
+ """
+
+
+class ValueAxisSubsetDescriptor(SimpleDescriptor):
+ """Single value of a discrete or continuous axis to use in a variable font.
+
+ .. versionadded:: 5.0
+ """
+ flavor = "axis-subset"
+ _attrs = ('name', 'userValue')
+
+ def __init__(self, *, name, userValue):
+ self.name: str = name
+ """Name of the :class:`AxisDescriptor` or :class:`DiscreteAxisDescriptor`
+ to "snapshot" or "freeze".
+ """
+ self.userValue: float = userValue
+ """Value in user coordinates at which to freeze the given axis."""
+
+
class BaseDocWriter(object):
_whiteSpace = " "
- ruleDescriptorClass = RuleDescriptor
axisDescriptorClass = AxisDescriptor
+ discreteAxisDescriptorClass = DiscreteAxisDescriptor
+ axisLabelDescriptorClass = AxisLabelDescriptor
+ locationLabelDescriptorClass = LocationLabelDescriptor
+ ruleDescriptorClass = RuleDescriptor
sourceDescriptorClass = SourceDescriptor
+ variableFontDescriptorClass = VariableFontDescriptor
+ valueAxisSubsetDescriptorClass = ValueAxisSubsetDescriptor
+ rangeAxisSubsetDescriptorClass = RangeAxisSubsetDescriptor
instanceDescriptorClass = InstanceDescriptor
@classmethod
@@ -422,21 +1297,29 @@ class BaseDocWriter(object):
def getRuleDescriptor(cls):
return cls.ruleDescriptorClass()
- def __init__(self, documentPath, documentObject):
+ def __init__(self, documentPath, documentObject: DesignSpaceDocument):
self.path = documentPath
self.documentObject = documentObject
- self.documentVersion = "4.1"
+ self.effectiveFormatTuple = self._getEffectiveFormatTuple()
self.root = ET.Element("designspace")
- self.root.attrib['format'] = self.documentVersion
- self._axes = [] # for use by the writer only
- self._rules = [] # for use by the writer only
def write(self, pretty=True, encoding="UTF-8", xml_declaration=True):
- if self.documentObject.axes:
- self.root.append(ET.Element("axes"))
+ self.root.attrib['format'] = ".".join(str(i) for i in self.effectiveFormatTuple)
+
+ if self.documentObject.axes or self.documentObject.elidedFallbackName is not None:
+ axesElement = ET.Element("axes")
+ if self.documentObject.elidedFallbackName is not None:
+ axesElement.attrib['elidedfallbackname'] = self.documentObject.elidedFallbackName
+ self.root.append(axesElement)
for axisObject in self.documentObject.axes:
self._addAxis(axisObject)
+ if self.documentObject.locationLabels:
+ labelsElement = ET.Element("labels")
+ for labelObject in self.documentObject.locationLabels:
+ self._addLocationLabel(labelsElement, labelObject)
+ self.root.append(labelsElement)
+
if self.documentObject.rules:
if getattr(self.documentObject, "rulesProcessingLast", False):
attributes = {"processing": "last"}
@@ -451,13 +1334,19 @@ class BaseDocWriter(object):
for sourceObject in self.documentObject.sources:
self._addSource(sourceObject)
+ if self.documentObject.variableFonts:
+ variableFontsElement = ET.Element("variable-fonts")
+ for variableFont in self.documentObject.variableFonts:
+ self._addVariableFont(variableFontsElement, variableFont)
+ self.root.append(variableFontsElement)
+
if self.documentObject.instances:
self.root.append(ET.Element("instances"))
for instanceObject in self.documentObject.instances:
self._addInstance(instanceObject)
if self.documentObject.lib:
- self._addLib(self.documentObject.lib)
+ self._addLib(self.root, self.documentObject.lib, 2)
tree = ET.ElementTree(self.root)
tree.write(
@@ -468,6 +1357,34 @@ class BaseDocWriter(object):
pretty_print=pretty,
)
+ def _getEffectiveFormatTuple(self):
+ """Try to use the version specified in the document, or a sufficiently
+ recent version to be able to encode what the document contains.
+ """
+ minVersion = self.documentObject.formatTuple
+ if (
+ any(
+ hasattr(axis, 'values') or
+ axis.axisOrdering is not None or
+ axis.axisLabels
+ for axis in self.documentObject.axes
+ ) or
+ self.documentObject.locationLabels or
+ any(
+ source.localisedFamilyName
+ for source in self.documentObject.sources
+ ) or
+ self.documentObject.variableFonts or
+ any(
+ instance.locationLabel or
+ instance.userLocation
+ for instance in self.documentObject.instances
+ )
+ ):
+ if minVersion < (5, 0):
+ minVersion = (5, 0)
+ return minVersion
+
def _makeLocationElement(self, locationObject, name=None):
""" Convert Location dict to a locationElement."""
locElement = ET.Element("location")
@@ -492,11 +1409,10 @@ class BaseDocWriter(object):
def intOrFloat(self, num):
if int(num) == num:
return "%d" % num
- return "%f" % num
+ return ("%f" % num).rstrip('0').rstrip('.')
def _addRule(self, ruleObject):
# if none of the conditions have minimum or maximum values, do not add the rule.
- self._rules.append(ruleObject)
ruleElement = ET.Element('rule')
if ruleObject.name is not None:
ruleElement.attrib['name'] = ruleObject.name
@@ -524,32 +1440,102 @@ class BaseDocWriter(object):
self.root.findall('.rules')[0].append(ruleElement)
def _addAxis(self, axisObject):
- self._axes.append(axisObject)
axisElement = ET.Element('axis')
axisElement.attrib['tag'] = axisObject.tag
axisElement.attrib['name'] = axisObject.name
- axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum)
- axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum)
- axisElement.attrib['default'] = self.intOrFloat(axisObject.default)
- if axisObject.hidden:
- axisElement.attrib['hidden'] = "1"
- for languageCode, labelName in sorted(axisObject.labelNames.items()):
- languageElement = ET.Element('labelname')
- languageElement.attrib[XML_LANG] = languageCode
- languageElement.text = labelName
- axisElement.append(languageElement)
+ self._addLabelNames(axisElement, axisObject.labelNames)
if axisObject.map:
for inputValue, outputValue in axisObject.map:
mapElement = ET.Element('map')
mapElement.attrib['input'] = self.intOrFloat(inputValue)
mapElement.attrib['output'] = self.intOrFloat(outputValue)
axisElement.append(mapElement)
+ if axisObject.axisOrdering or axisObject.axisLabels:
+ labelsElement = ET.Element('labels')
+ if axisObject.axisOrdering is not None:
+ labelsElement.attrib['ordering'] = str(axisObject.axisOrdering)
+ for label in axisObject.axisLabels:
+ self._addAxisLabel(labelsElement, label)
+ axisElement.append(labelsElement)
+ if hasattr(axisObject, "minimum"):
+ axisElement.attrib['minimum'] = self.intOrFloat(axisObject.minimum)
+ axisElement.attrib['maximum'] = self.intOrFloat(axisObject.maximum)
+ 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:
+ axisElement.attrib['hidden'] = "1"
self.root.findall('.axes')[0].append(axisElement)
+ def _addAxisLabel(self, axisElement: ET.Element, label: AxisLabelDescriptor) -> None:
+ labelElement = ET.Element('label')
+ labelElement.attrib['uservalue'] = self.intOrFloat(label.userValue)
+ if label.userMinimum is not None:
+ labelElement.attrib['userminimum'] = self.intOrFloat(label.userMinimum)
+ if label.userMaximum is not None:
+ labelElement.attrib['usermaximum'] = self.intOrFloat(label.userMaximum)
+ labelElement.attrib['name'] = label.name
+ if label.elidable:
+ labelElement.attrib['elidable'] = "true"
+ if label.olderSibling:
+ labelElement.attrib['oldersibling'] = "true"
+ if label.linkedUserValue is not None:
+ labelElement.attrib['linkeduservalue'] = self.intOrFloat(label.linkedUserValue)
+ self._addLabelNames(labelElement, label.labelNames)
+ axisElement.append(labelElement)
+
+ def _addLabelNames(self, parentElement, labelNames):
+ for languageCode, labelName in sorted(labelNames.items()):
+ languageElement = ET.Element('labelname')
+ languageElement.attrib[XML_LANG] = languageCode
+ languageElement.text = labelName
+ parentElement.append(languageElement)
+
+ def _addLocationLabel(self, parentElement: ET.Element, label: LocationLabelDescriptor) -> None:
+ labelElement = ET.Element('label')
+ labelElement.attrib['name'] = label.name
+ if label.elidable:
+ labelElement.attrib['elidable'] = "true"
+ if label.olderSibling:
+ labelElement.attrib['oldersibling'] = "true"
+ self._addLabelNames(labelElement, label.labelNames)
+ self._addLocationElement(labelElement, userLocation=label.userLocation)
+ parentElement.append(labelElement)
+
+ def _addLocationElement(
+ self,
+ parentElement,
+ *,
+ designLocation: AnisotropicLocationDict = None,
+ userLocation: SimpleLocationDict = None
+ ):
+ locElement = ET.Element("location")
+ for axis in self.documentObject.axes:
+ if designLocation is not None and axis.name in designLocation:
+ dimElement = ET.Element('dimension')
+ dimElement.attrib['name'] = axis.name
+ value = designLocation[axis.name]
+ if isinstance(value, tuple):
+ dimElement.attrib['xvalue'] = self.intOrFloat(value[0])
+ dimElement.attrib['yvalue'] = self.intOrFloat(value[1])
+ else:
+ dimElement.attrib['xvalue'] = self.intOrFloat(value)
+ locElement.append(dimElement)
+ elif userLocation is not None and axis.name in userLocation:
+ dimElement = ET.Element('dimension')
+ dimElement.attrib['name'] = axis.name
+ value = userLocation[axis.name]
+ dimElement.attrib['uservalue'] = self.intOrFloat(value)
+ locElement.append(dimElement)
+ if len(locElement) > 0:
+ parentElement.append(locElement)
+
def _addInstance(self, instanceObject):
instanceElement = ET.Element('instance')
if instanceObject.name is not None:
instanceElement.attrib['name'] = instanceObject.name
+ if instanceObject.locationLabel is not None:
+ instanceElement.attrib['location'] = instanceObject.locationLabel
if instanceObject.familyName is not None:
instanceElement.attrib['familyname'] = instanceObject.familyName
if instanceObject.styleName is not None:
@@ -596,9 +1582,19 @@ class BaseDocWriter(object):
localisedStyleMapFamilyNameElement.text = instanceObject.getStyleMapFamilyName(code)
instanceElement.append(localisedStyleMapFamilyNameElement)
- if instanceObject.location is not None:
- locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location)
- instanceElement.append(locationElement)
+ if self.effectiveFormatTuple >= (5, 0):
+ if instanceObject.locationLabel is None:
+ self._addLocationElement(
+ instanceElement,
+ designLocation=instanceObject.designLocation,
+ userLocation=instanceObject.userLocation
+ )
+ else:
+ # Pre-version 5.0 code was validating and filling in the location
+ # dict while writing it out, as preserved below.
+ if instanceObject.location is not None:
+ locationElement, instanceObject.location = self._makeLocationElement(instanceObject.location)
+ instanceElement.append(locationElement)
if instanceObject.filename is not None:
instanceElement.attrib['filename'] = instanceObject.filename
if instanceObject.postScriptFontName is not None:
@@ -607,24 +1603,23 @@ class BaseDocWriter(object):
instanceElement.attrib['stylemapfamilyname'] = instanceObject.styleMapFamilyName
if instanceObject.styleMapStyleName is not None:
instanceElement.attrib['stylemapstylename'] = instanceObject.styleMapStyleName
- if instanceObject.glyphs:
- if instanceElement.findall('.glyphs') == []:
- glyphsElement = ET.Element('glyphs')
- instanceElement.append(glyphsElement)
- glyphsElement = instanceElement.findall('.glyphs')[0]
- for glyphName, data in sorted(instanceObject.glyphs.items()):
- glyphElement = self._writeGlyphElement(instanceElement, instanceObject, glyphName, data)
- glyphsElement.append(glyphElement)
- if instanceObject.kerning:
- kerningElement = ET.Element('kerning')
- instanceElement.append(kerningElement)
- if instanceObject.info:
- infoElement = ET.Element('info')
- instanceElement.append(infoElement)
- if instanceObject.lib:
- libElement = ET.Element('lib')
- libElement.append(plistlib.totree(instanceObject.lib, indent_level=4))
- instanceElement.append(libElement)
+ if self.effectiveFormatTuple < (5, 0):
+ # Deprecated members as of version 5.0
+ if instanceObject.glyphs:
+ if instanceElement.findall('.glyphs') == []:
+ glyphsElement = ET.Element('glyphs')
+ instanceElement.append(glyphsElement)
+ glyphsElement = instanceElement.findall('.glyphs')[0]
+ for glyphName, data in sorted(instanceObject.glyphs.items()):
+ glyphElement = self._writeGlyphElement(instanceElement, instanceObject, glyphName, data)
+ glyphsElement.append(glyphElement)
+ if instanceObject.kerning:
+ kerningElement = ET.Element('kerning')
+ instanceElement.append(kerningElement)
+ if instanceObject.info:
+ infoElement = ET.Element('info')
+ instanceElement.append(infoElement)
+ self._addLib(instanceElement, instanceObject.lib, 4)
self.root.findall('.instances')[0].append(instanceElement)
def _addSource(self, sourceObject):
@@ -641,6 +1636,16 @@ class BaseDocWriter(object):
sourceElement.attrib['stylename'] = sourceObject.styleName
if sourceObject.layerName is not None:
sourceElement.attrib['layer'] = sourceObject.layerName
+ if sourceObject.localisedFamilyName:
+ languageCodes = list(sourceObject.localisedFamilyName.keys())
+ languageCodes.sort()
+ for code in languageCodes:
+ if code == "en":
+ continue # already stored in the element attribute
+ localisedFamilyNameElement = ET.Element('familyname')
+ localisedFamilyNameElement.attrib[XML_LANG] = code
+ localisedFamilyNameElement.text = sourceObject.getFamilyName(code)
+ sourceElement.append(localisedFamilyNameElement)
if sourceObject.copyLib:
libElement = ET.Element('lib')
libElement.attrib['copy'] = "1"
@@ -670,14 +1675,50 @@ class BaseDocWriter(object):
glyphElement.attrib["name"] = name
glyphElement.attrib["mute"] = '1'
sourceElement.append(glyphElement)
- locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location)
- sourceElement.append(locationElement)
+ if self.effectiveFormatTuple >= (5, 0):
+ self._addLocationElement(sourceElement, designLocation=sourceObject.location)
+ else:
+ # Pre-version 5.0 code was validating and filling in the location
+ # dict while writing it out, as preserved below.
+ locationElement, sourceObject.location = self._makeLocationElement(sourceObject.location)
+ sourceElement.append(locationElement)
self.root.findall('.sources')[0].append(sourceElement)
- def _addLib(self, dict):
+ def _addVariableFont(self, parentElement: ET.Element, vf: VariableFontDescriptor) -> None:
+ vfElement = ET.Element('variable-font')
+ vfElement.attrib['name'] = vf.name
+ if vf.filename is not None:
+ vfElement.attrib['filename'] = vf.filename
+ if vf.axisSubsets:
+ subsetsElement = ET.Element('axis-subsets')
+ for subset in vf.axisSubsets:
+ subsetElement = ET.Element('axis-subset')
+ subsetElement.attrib['name'] = subset.name
+ # 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 hasattr(subset, "userValue"):
+ subset = cast(ValueAxisSubsetDescriptor, subset)
+ subsetElement.attrib['uservalue'] = self.intOrFloat(subset.userValue)
+ subsetsElement.append(subsetElement)
+ vfElement.append(subsetsElement)
+ self._addLib(vfElement, vf.lib, 4)
+ parentElement.append(vfElement)
+
+ def _addLib(self, parentElement: ET.Element, data: Any, indent_level: int) -> None:
+ if not data:
+ return
libElement = ET.Element('lib')
- libElement.append(plistlib.totree(dict, indent_level=2))
- self.root.append(libElement)
+ libElement.append(plistlib.totree(data, indent_level=indent_level))
+ parentElement.append(libElement)
def _writeGlyphElement(self, instanceElement, instanceObject, glyphName, data):
glyphElement = ET.Element('glyph')
@@ -711,9 +1752,15 @@ class BaseDocWriter(object):
class BaseDocReader(LogMixin):
- ruleDescriptorClass = RuleDescriptor
axisDescriptorClass = AxisDescriptor
+ discreteAxisDescriptorClass = DiscreteAxisDescriptor
+ axisLabelDescriptorClass = AxisLabelDescriptor
+ locationLabelDescriptorClass = LocationLabelDescriptor
+ ruleDescriptorClass = RuleDescriptor
sourceDescriptorClass = SourceDescriptor
+ variableFontsDescriptorClass = VariableFontDescriptor
+ valueAxisSubsetDescriptorClass = ValueAxisSubsetDescriptor
+ rangeAxisSubsetDescriptorClass = RangeAxisSubsetDescriptor
instanceDescriptorClass = InstanceDescriptor
def __init__(self, documentPath, documentObject):
@@ -738,7 +1785,9 @@ class BaseDocReader(LogMixin):
def read(self):
self.readAxes()
+ self.readLabels()
self.readRules()
+ self.readVariableFonts()
self.readSources()
self.readInstances()
self.readLib()
@@ -810,17 +1859,24 @@ class BaseDocReader(LogMixin):
def readAxes(self):
# read the axes elements, including the warp map.
+ axesElement = self.root.find(".axes")
+ if axesElement is not None and 'elidedfallbackname' in axesElement.attrib:
+ self.documentObject.elidedFallbackName = axesElement.attrib['elidedfallbackname']
axisElements = self.root.findall(".axes/axis")
if not axisElements:
return
for axisElement in axisElements:
- axisObject = self.axisDescriptorClass()
+ if self.documentObject.formatTuple >= (5, 0) and "values" in axisElement.attrib:
+ axisObject = self.discreteAxisDescriptorClass()
+ axisObject.values = [float(s) for s in axisElement.attrib["values"].split(" ")]
+ else:
+ axisObject = self.axisDescriptorClass()
+ axisObject.minimum = float(axisElement.attrib.get("minimum"))
+ axisObject.maximum = float(axisElement.attrib.get("maximum"))
+ axisObject.default = float(axisElement.attrib.get("default"))
axisObject.name = axisElement.attrib.get("name")
- axisObject.minimum = float(axisElement.attrib.get("minimum"))
- axisObject.maximum = float(axisElement.attrib.get("maximum"))
if axisElement.attrib.get('hidden', False):
axisObject.hidden = True
- axisObject.default = float(axisElement.attrib.get("default"))
axisObject.tag = axisElement.attrib.get("tag")
for mapElement in axisElement.findall('map'):
a = float(mapElement.attrib['input'])
@@ -832,9 +1888,172 @@ class BaseDocReader(LogMixin):
for key, lang in labelNameElement.items():
if key == XML_LANG:
axisObject.labelNames[lang] = tostr(labelNameElement.text)
+ labelElement = axisElement.find(".labels")
+ if labelElement is not None:
+ if "ordering" in labelElement.attrib:
+ axisObject.axisOrdering = int(labelElement.attrib["ordering"])
+ for label in labelElement.findall(".label"):
+ axisObject.axisLabels.append(self.readAxisLabel(label))
self.documentObject.axes.append(axisObject)
self.axisDefaults[axisObject.name] = axisObject.default
+ def readAxisLabel(self, element: ET.Element):
+ xml_attrs = {'userminimum', 'uservalue', 'usermaximum', 'name', 'elidable', 'oldersibling', 'linkeduservalue'}
+ unknown_attrs = set(element.attrib) - xml_attrs
+ if unknown_attrs:
+ raise DesignSpaceDocumentError(f"label element contains unknown attributes: {', '.join(unknown_attrs)}")
+
+ name = element.get("name")
+ if name is None:
+ raise DesignSpaceDocumentError("label element must have a name attribute.")
+ valueStr = element.get("uservalue")
+ if valueStr is None:
+ raise DesignSpaceDocumentError("label element must have a uservalue attribute.")
+ value = float(valueStr)
+ minimumStr = element.get("userminimum")
+ minimum = float(minimumStr) if minimumStr is not None else None
+ maximumStr = element.get("usermaximum")
+ maximum = float(maximumStr) if maximumStr is not None else None
+ linkedValueStr = element.get("linkeduservalue")
+ linkedValue = float(linkedValueStr) if linkedValueStr is not None else None
+ elidable = True if element.get("elidable") == "true" else False
+ olderSibling = True if element.get("oldersibling") == "true" else False
+ labelNames = {
+ lang: label_name.text or ""
+ for label_name in element.findall("labelname")
+ for attr, lang in label_name.items()
+ if attr == XML_LANG
+ # Note: elementtree reads the "xml:lang" attribute name as
+ # '{http://www.w3.org/XML/1998/namespace}lang'
+ }
+ return self.axisLabelDescriptorClass(
+ name=name,
+ userValue=value,
+ userMinimum=minimum,
+ userMaximum=maximum,
+ elidable=elidable,
+ olderSibling=olderSibling,
+ linkedUserValue=linkedValue,
+ labelNames=labelNames,
+ )
+
+ def readLabels(self):
+ if self.documentObject.formatTuple < (5, 0):
+ return
+
+ xml_attrs = {'name', 'elidable', 'oldersibling'}
+ for labelElement in self.root.findall(".labels/label"):
+ unknown_attrs = set(labelElement.attrib) - xml_attrs
+ if unknown_attrs:
+ raise DesignSpaceDocumentError(f"Label element contains unknown attributes: {', '.join(unknown_attrs)}")
+
+ name = labelElement.get("name")
+ if name is None:
+ raise DesignSpaceDocumentError("label element must have a name attribute.")
+ designLocation, userLocation = self.locationFromElement(labelElement)
+ if designLocation:
+ raise DesignSpaceDocumentError(f'<label> element "{name}" must only have user locations (using uservalue="").')
+ elidable = True if labelElement.get("elidable") == "true" else False
+ olderSibling = True if labelElement.get("oldersibling") == "true" else False
+ labelNames = {
+ lang: label_name.text or ""
+ for label_name in labelElement.findall("labelname")
+ for attr, lang in label_name.items()
+ if attr == XML_LANG
+ # Note: elementtree reads the "xml:lang" attribute name as
+ # '{http://www.w3.org/XML/1998/namespace}lang'
+ }
+ locationLabel = self.locationLabelDescriptorClass(
+ name=name,
+ userLocation=userLocation,
+ elidable=elidable,
+ olderSibling=olderSibling,
+ labelNames=labelNames,
+ )
+ self.documentObject.locationLabels.append(locationLabel)
+
+ def readVariableFonts(self):
+ if self.documentObject.formatTuple < (5, 0):
+ return
+
+ xml_attrs = {'name', 'filename'}
+ for variableFontElement in self.root.findall(".variable-fonts/variable-font"):
+ unknown_attrs = set(variableFontElement.attrib) - xml_attrs
+ if unknown_attrs:
+ raise DesignSpaceDocumentError(f"variable-font element contains unknown attributes: {', '.join(unknown_attrs)}")
+
+ name = variableFontElement.get("name")
+ if name is None:
+ raise DesignSpaceDocumentError("variable-font element must have a name attribute.")
+
+ filename = variableFontElement.get("filename")
+
+ axisSubsetsElement = variableFontElement.find(".axis-subsets")
+ if axisSubsetsElement is None:
+ raise DesignSpaceDocumentError("variable-font element must contain an axis-subsets element.")
+ axisSubsets = []
+ for axisSubset in axisSubsetsElement.iterfind(".axis-subset"):
+ axisSubsets.append(self.readAxisSubset(axisSubset))
+
+ lib = None
+ libElement = variableFontElement.find(".lib")
+ if libElement is not None:
+ lib = plistlib.fromtree(libElement[0])
+
+ variableFont = self.variableFontsDescriptorClass(
+ name=name,
+ filename=filename,
+ axisSubsets=axisSubsets,
+ lib=lib,
+ )
+ self.documentObject.variableFonts.append(variableFont)
+
+ def readAxisSubset(self, element: ET.Element):
+ if "uservalue" in element.attrib:
+ xml_attrs = {'name', 'uservalue'}
+ unknown_attrs = set(element.attrib) - xml_attrs
+ if unknown_attrs:
+ raise DesignSpaceDocumentError(f"axis-subset element contains unknown attributes: {', '.join(unknown_attrs)}")
+
+ name = element.get("name")
+ if name is None:
+ raise DesignSpaceDocumentError("axis-subset element must have a name attribute.")
+ userValueStr = element.get("uservalue")
+ if userValueStr is None:
+ raise DesignSpaceDocumentError(
+ "The axis-subset element for a discrete subset must have a uservalue attribute."
+ )
+ userValue = float(userValueStr)
+
+ return self.valueAxisSubsetDescriptorClass(name=name, userValue=userValue)
+ else:
+ xml_attrs = {'name', 'userminimum', 'userdefault', 'usermaximum'}
+ unknown_attrs = set(element.attrib) - xml_attrs
+ if unknown_attrs:
+ raise DesignSpaceDocumentError(f"axis-subset element contains unknown attributes: {', '.join(unknown_attrs)}")
+
+ name = element.get("name")
+ if name is None:
+ raise DesignSpaceDocumentError("axis-subset element must have a name attribute.")
+
+ userMinimum = element.get("userminimum")
+ userDefault = element.get("userdefault")
+ userMaximum = element.get("usermaximum")
+ if userMinimum is not None and userDefault is not None and userMaximum is not None:
+ return self.rangeAxisSubsetDescriptorClass(
+ name=name,
+ userMinimum=float(userMinimum),
+ userDefault=float(userDefault),
+ userMaximum=float(userMaximum),
+ )
+ if all(v is None for v in (userMinimum, userDefault, userMaximum)):
+ return self.rangeAxisSubsetDescriptorClass(name=name)
+
+ raise DesignSpaceDocumentError(
+ "axis-subset element must have min/max/default values or none at all."
+ )
+
+
def readSources(self):
for sourceCount, sourceElement in enumerate(self.root.findall(".sources/source")):
filename = sourceElement.attrib.get('filename')
@@ -856,7 +2075,15 @@ class BaseDocReader(LogMixin):
styleName = sourceElement.attrib.get("stylename")
if styleName is not None:
sourceObject.styleName = styleName
- sourceObject.location = self.locationFromElement(sourceElement)
+ for familyNameElement in sourceElement.findall('familyname'):
+ for key, lang in familyNameElement.items():
+ if key == XML_LANG:
+ familyName = familyNameElement.text
+ sourceObject.setFamilyName(familyName, lang)
+ designLocation, userLocation = self.locationFromElement(sourceElement)
+ if userLocation:
+ raise DesignSpaceDocumentError(f'<source> element "{sourceName}" must only have design locations (using xvalue="").')
+ sourceObject.location = designLocation
layerName = sourceElement.attrib.get('layer')
if layerName is not None:
sourceObject.layerName = layerName
@@ -886,40 +2113,63 @@ class BaseDocReader(LogMixin):
self.documentObject.sources.append(sourceObject)
def locationFromElement(self, element):
- elementLocation = None
+ """Read a nested ``<location>`` element inside the given ``element``.
+
+ .. versionchanged:: 5.0
+ Return a tuple of (designLocation, userLocation)
+ """
+ elementLocation = (None, None)
for locationElement in element.findall('.location'):
elementLocation = self.readLocationElement(locationElement)
break
return elementLocation
def readLocationElement(self, locationElement):
- """ Format 0 location reader """
+ """Read a ``<location>`` element.
+
+ .. versionchanged:: 5.0
+ Return a tuple of (designLocation, userLocation)
+ """
if self._strictAxisNames and not self.documentObject.axes:
raise DesignSpaceDocumentError("No axes defined")
- loc = {}
+ userLoc = {}
+ designLoc = {}
for dimensionElement in locationElement.findall(".dimension"):
dimName = dimensionElement.attrib.get("name")
if self._strictAxisNames and dimName not in self.axisDefaults:
# In case the document contains no axis definitions,
self.log.warning("Location with undefined axis: \"%s\".", dimName)
continue
- xValue = yValue = None
+ userValue = xValue = yValue = None
+ try:
+ userValue = dimensionElement.attrib.get('uservalue')
+ if userValue is not None:
+ userValue = float(userValue)
+ except ValueError:
+ self.log.warning("ValueError in readLocation userValue %3.3f", userValue)
try:
xValue = dimensionElement.attrib.get('xvalue')
- xValue = float(xValue)
+ if xValue is not None:
+ xValue = float(xValue)
except ValueError:
- self.log.warning("KeyError in readLocation xValue %3.3f", xValue)
+ self.log.warning("ValueError in readLocation xValue %3.3f", xValue)
try:
yValue = dimensionElement.attrib.get('yvalue')
if yValue is not None:
yValue = float(yValue)
except ValueError:
- pass
+ self.log.warning("ValueError in readLocation yValue %3.3f", yValue)
+ if userValue is None == xValue is None:
+ raise DesignSpaceDocumentError(f'Exactly one of uservalue="" or xvalue="" must be provided for location dimension "{dimName}"')
if yValue is not None:
- loc[dimName] = (xValue, yValue)
+ if xValue is None:
+ raise DesignSpaceDocumentError(f'Missing xvalue="" for the location dimension "{dimName}"" with yvalue="{yValue}"')
+ designLoc[dimName] = (xValue, yValue)
+ elif xValue is not None:
+ designLoc[dimName] = xValue
else:
- loc[dimName] = xValue
- return loc
+ userLoc[dimName] = userValue
+ return designLoc, userLoc
def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True):
instanceElements = self.root.findall('.instances/instance')
@@ -974,9 +2224,13 @@ class BaseDocReader(LogMixin):
if key == XML_LANG:
styleMapFamilyName = styleMapFamilyNameElement.text
instanceObject.setStyleMapFamilyName(styleMapFamilyName, lang)
- instanceLocation = self.locationFromElement(instanceElement)
- if instanceLocation is not None:
- instanceObject.location = instanceLocation
+ designLocation, userLocation = self.locationFromElement(instanceElement)
+ locationLabel = instanceElement.attrib.get('location')
+ if (designLocation or userLocation) and locationLabel is not None:
+ raise DesignSpaceDocumentError('instance element must have at most one of the location="..." attribute or the nested location element')
+ instanceObject.locationLabel = locationLabel
+ instanceObject.userLocation = userLocation or {}
+ instanceObject.designLocation = designLocation or {}
for glyphElement in instanceElement.findall('.glyphs/glyph'):
self.readGlyphElement(glyphElement, instanceObject)
for infoElement in instanceElement.findall("info"):
@@ -993,19 +2247,16 @@ class BaseDocReader(LogMixin):
""" Read the info element."""
instanceObject.info = True
- def readKerningElement(self, kerningElement, instanceObject):
- """ Read the kerning element."""
- kerningLocation = self.locationFromElement(kerningElement)
- instanceObject.addKerning(kerningLocation)
-
def readGlyphElement(self, glyphElement, instanceObject):
"""
- Read the glyph element:
+ Read the glyph element, which could look like either one of these:
.. code-block:: xml
<glyph name="b" unicode="0x62"/>
+
<glyph name="b"/>
+
<glyph name="b">
<master location="location-token-bbb" source="master-token-aaa2"/>
<master glyphname="b.alt1" location="location-token-ccc" source="master-token-aaa3"/>
@@ -1033,19 +2284,23 @@ class BaseDocReader(LogMixin):
for noteElement in glyphElement.findall('.note'):
glyphData['note'] = noteElement.text
break
- instanceLocation = self.locationFromElement(glyphElement)
- if instanceLocation is not None:
- glyphData['instanceLocation'] = instanceLocation
+ designLocation, userLocation = self.locationFromElement(glyphElement)
+ if userLocation:
+ raise DesignSpaceDocumentError(f'<glyph> element "{glyphName}" must only have design locations (using xvalue="").')
+ if designLocation is not None:
+ glyphData['instanceLocation'] = designLocation
glyphSources = None
for masterElement in glyphElement.findall('.masters/master'):
fontSourceName = masterElement.attrib.get('source')
- sourceLocation = self.locationFromElement(masterElement)
+ designLocation, userLocation = self.locationFromElement(masterElement)
+ if userLocation:
+ raise DesignSpaceDocumentError(f'<master> element "{fontSourceName}" must only have design locations (using xvalue="").')
masterGlyphName = masterElement.attrib.get('glyphname')
if masterGlyphName is None:
# if we don't read a glyphname, use the one we have
masterGlyphName = glyphName
d = dict(font=fontSourceName,
- location=sourceLocation,
+ location=designLocation,
glyphName=masterGlyphName)
if glyphSources is None:
glyphSources = []
@@ -1061,9 +2316,43 @@ class BaseDocReader(LogMixin):
class DesignSpaceDocument(LogMixin, AsDictMixin):
- """ Read, write data from the designspace file"""
+ """The DesignSpaceDocument object can read and write ``.designspace`` data.
+ It imports the axes, sources, variable fonts and instances to very basic
+ **descriptor** objects that store the data in attributes. Data is added to
+ the document by creating such descriptor objects, filling them with data
+ and then adding them to the document. This makes it easy to integrate this
+ object in different contexts.
+
+ The **DesignSpaceDocument** object can be subclassed to work with
+ different objects, as long as they have the same attributes. Reader and
+ Writer objects can be subclassed as well.
+
+ **Note:** Python attribute names are usually camelCased, the
+ corresponding `XML <document-xml-structure>`_ attributes are usually
+ all lowercase.
+
+ .. code:: python
+
+ from fontTools.designspaceLib import DesignSpaceDocument
+ doc = DesignSpaceDocument.fromfile("some/path/to/my.designspace")
+ doc.formatVersion
+ doc.elidedFallbackName
+ doc.axes
+ doc.locationLabels
+ doc.rules
+ doc.rulesProcessingLast
+ doc.sources
+ doc.variableFonts
+ doc.instances
+ doc.lib
+
+ """
+
def __init__(self, readerClass=None, writerClass=None):
self.path = None
+ """String, optional. When the document is read from the disk, this is
+ the full path that was given to :meth:`read` or :meth:`fromfile`.
+ """
self.filename = None
"""String, optional. When the document is read from the disk, this is
its original file name, i.e. the last part of its path.
@@ -1073,18 +2362,70 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
possible "good" filename, in case one wants to save the file somewhere.
"""
- self.formatVersion = None
- self.sources = []
- self.instances = []
- self.axes = []
- self.rules = []
- self.rulesProcessingLast = False
- self.default = None # name of the default master
+ self.formatVersion: Optional[str] = None
+ """Format version for this document, as a string. E.g. "4.0" """
- self.lib = {}
- """Custom data associated with the whole document."""
+ self.elidedFallbackName: Optional[str] = None
+ """STAT Style Attributes Header field ``elidedFallbackNameID``.
+
+ See: `OTSpec STAT Style Attributes Header <https://docs.microsoft.com/en-us/typography/opentype/spec/stat#style-attributes-header>`_
+
+ .. versionadded:: 5.0
+ """
+
+ self.axes: List[Union[AxisDescriptor, DiscreteAxisDescriptor]] = []
+ """List of this document's axes."""
+ self.locationLabels: List[LocationLabelDescriptor] = []
+ """List of this document's STAT format 4 labels.
+
+ .. versionadded:: 5.0"""
+ self.rules: List[RuleDescriptor] = []
+ """List of this document's rules."""
+ self.rulesProcessingLast: bool = False
+ """This flag indicates whether the substitution rules should be applied
+ before or after other glyph substitution features.
+
+ - False: before
+ - True: after.
+
+ Default is False. For new projects, you probably want True. See
+ the following issues for more information:
+ `fontTools#1371 <https://github.com/fonttools/fonttools/issues/1371#issuecomment-590214572>`__
+ `fontTools#2050 <https://github.com/fonttools/fonttools/issues/2050#issuecomment-678691020>`__
+
+ If you want to use a different feature altogether, e.g. ``calt``,
+ use the lib key ``com.github.fonttools.varLib.featureVarsFeatureTag``
+
+ .. code:: xml
+
+ <lib>
+ <dict>
+ <key>com.github.fonttools.varLib.featureVarsFeatureTag</key>
+ <string>calt</string>
+ </dict>
+ </lib>
+ """
+ self.sources: List[SourceDescriptor] = []
+ """List of this document's sources."""
+ self.variableFonts: List[VariableFontDescriptor] = []
+ """List of this document's variable fonts.
+
+ .. versionadded:: 5.0"""
+ self.instances: List[InstanceDescriptor] = []
+ """List of this document's instances."""
+ self.lib: Dict = {}
+ """User defined, custom data associated with the whole document.
+
+ Use reverse-DNS notation to identify your own data.
+ Respect the data stored by others.
+ """
+
+ self.default: Optional[str] = None
+ """Name of the default master.
+
+ This attribute is updated by the :meth:`findDefault`
+ """
- #
if readerClass is not None:
self.readerClass = readerClass
else:
@@ -1096,6 +2437,9 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
@classmethod
def fromfile(cls, path, readerClass=None, writerClass=None):
+ """Read a designspace file from ``path`` and return a new instance of
+ :class:.
+ """
self = cls(readerClass=readerClass, writerClass=writerClass)
self.read(path)
return self
@@ -1110,6 +2454,7 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
return self
def tostring(self, encoding=None):
+ """Returns the designspace as a string. Default encoding ``utf-8``."""
if encoding is str or (
encoding is not None and encoding.lower() == "unicode"
):
@@ -1126,6 +2471,9 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
return f.getvalue()
def read(self, path):
+ """Read a designspace file from ``path`` and populates the fields of
+ ``self`` with the data.
+ """
if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__()
self.path = path
@@ -1136,6 +2484,7 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
self.findDefault()
def write(self, path):
+ """Write this designspace to ``path``."""
if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__()
self.path = path
@@ -1150,8 +2499,10 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
def updatePaths(self):
"""
- Right before we save we need to identify and respond to the following situations:
- In each descriptor, we have to do the right thing for the filename attribute.
+ Right before we save we need to identify and respond to the following situations:
+ In each descriptor, we have to do the right thing for the filename attribute.
+
+ ::
case 1.
descriptor.filename == None
@@ -1187,8 +2538,6 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
there is a conflict between the given filename, and the path.
So we know where the file is relative to the document.
Can't guess why they're different, we just choose for path to be correct and update filename.
-
-
"""
assert self.path is not None
for descriptor in self.sources + self.instances:
@@ -1196,40 +2545,96 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
# case 3 and 4: filename gets updated and relativized
descriptor.filename = self._posixRelativePath(descriptor.path)
- def addSource(self, sourceDescriptor):
+ def addSource(self, sourceDescriptor: SourceDescriptor):
+ """Add the given ``sourceDescriptor`` to ``doc.sources``."""
self.sources.append(sourceDescriptor)
def addSourceDescriptor(self, **kwargs):
+ """Instantiate a new :class:`SourceDescriptor` using the given
+ ``kwargs`` and add it to ``doc.sources``.
+ """
source = self.writerClass.sourceDescriptorClass(**kwargs)
self.addSource(source)
return source
- def addInstance(self, instanceDescriptor):
+ def addInstance(self, instanceDescriptor: InstanceDescriptor):
+ """Add the given ``instanceDescriptor`` to :attr:`instances`."""
self.instances.append(instanceDescriptor)
def addInstanceDescriptor(self, **kwargs):
+ """Instantiate a new :class:`InstanceDescriptor` using the given
+ ``kwargs`` and add it to :attr:`instances`.
+ """
instance = self.writerClass.instanceDescriptorClass(**kwargs)
self.addInstance(instance)
return instance
- def addAxis(self, axisDescriptor):
+ def addAxis(self, axisDescriptor: Union[AxisDescriptor, DiscreteAxisDescriptor]):
+ """Add the given ``axisDescriptor`` to :attr:`axes`."""
self.axes.append(axisDescriptor)
def addAxisDescriptor(self, **kwargs):
- axis = self.writerClass.axisDescriptorClass(**kwargs)
+ """Instantiate a new :class:`AxisDescriptor` using the given
+ ``kwargs`` and add it to :attr:`axes`.
+
+ The axis will be and instance of :class:`DiscreteAxisDescriptor` if
+ the ``kwargs`` provide a ``value``, or a :class:`AxisDescriptor` otherwise.
+ """
+ if "values" in kwargs:
+ axis = self.writerClass.discreteAxisDescriptorClass(**kwargs)
+ else:
+ axis = self.writerClass.axisDescriptorClass(**kwargs)
self.addAxis(axis)
return axis
- def addRule(self, ruleDescriptor):
+ def addRule(self, ruleDescriptor: RuleDescriptor):
+ """Add the given ``ruleDescriptor`` to :attr:`rules`."""
self.rules.append(ruleDescriptor)
def addRuleDescriptor(self, **kwargs):
+ """Instantiate a new :class:`RuleDescriptor` using the given
+ ``kwargs`` and add it to :attr:`rules`.
+ """
rule = self.writerClass.ruleDescriptorClass(**kwargs)
self.addRule(rule)
return rule
+ def addVariableFont(self, variableFontDescriptor: VariableFontDescriptor):
+ """Add the given ``variableFontDescriptor`` to :attr:`variableFonts`.
+
+ .. versionadded:: 5.0
+ """
+ self.variableFonts.append(variableFontDescriptor)
+
+ def addVariableFontDescriptor(self, **kwargs):
+ """Instantiate a new :class:`VariableFontDescriptor` using the given
+ ``kwargs`` and add it to :attr:`variableFonts`.
+
+ .. versionadded:: 5.0
+ """
+ variableFont = self.writerClass.variableFontDescriptorClass(**kwargs)
+ self.addVariableFont(variableFont)
+ return variableFont
+
+ def addLocationLabel(self, locationLabelDescriptor: LocationLabelDescriptor):
+ """Add the given ``locationLabelDescriptor`` to :attr:`locationLabels`.
+
+ .. versionadded:: 5.0
+ """
+ self.locationLabels.append(locationLabelDescriptor)
+
+ def addLocationLabelDescriptor(self, **kwargs):
+ """Instantiate a new :class:`LocationLabelDescriptor` using the given
+ ``kwargs`` and add it to :attr:`locationLabels`.
+
+ .. versionadded:: 5.0
+ """
+ locationLabel = self.writerClass.locationLabelDescriptorClass(**kwargs)
+ self.addLocationLabel(locationLabel)
+ return locationLabel
+
def newDefaultLocation(self):
- """Return default location in design space."""
+ """Return a dict with the default location in design space coordinates."""
# Without OrderedDict, output XML would be non-deterministic.
# https://github.com/LettError/designSpaceDocument/issues/10
loc = collections.OrderedDict()
@@ -1239,9 +2644,21 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
)
return loc
+ def labelForUserLocation(self, userLocation: SimpleLocationDict) -> Optional[LocationLabelDescriptor]:
+ """Return the :class:`LocationLabel` that matches the given
+ ``userLocation``, or ``None`` if no such label exists.
+
+ .. versionadded:: 5.0
+ """
+ return next(
+ (label for label in self.locationLabels if label.userLocation == userLocation), None
+ )
+
def updateFilenameFromPath(self, masters=True, instances=True, force=False):
- # set a descriptor filename attr from the path and this document path
- # if the filename attribute is not None: skip it.
+ """Set a descriptor filename attr from the path and this document path.
+
+ If the filename attribute is not None: skip it.
+ """
if masters:
for descriptor in self.sources:
if descriptor.filename is not None and not force:
@@ -1256,49 +2673,102 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
descriptor.filename = self._posixRelativePath(descriptor.path)
def newAxisDescriptor(self):
- # Ask the writer class to make us a new axisDescriptor
+ """Ask the writer class to make us a new axisDescriptor."""
return self.writerClass.getAxisDecriptor()
def newSourceDescriptor(self):
- # Ask the writer class to make us a new sourceDescriptor
+ """Ask the writer class to make us a new sourceDescriptor."""
return self.writerClass.getSourceDescriptor()
def newInstanceDescriptor(self):
- # Ask the writer class to make us a new instanceDescriptor
+ """Ask the writer class to make us a new instanceDescriptor."""
return self.writerClass.getInstanceDescriptor()
def getAxisOrder(self):
+ """Return a list of axis names, in the same order as defined in the document."""
names = []
for axisDescriptor in self.axes:
names.append(axisDescriptor.name)
return names
def getAxis(self, name):
+ """Return the axis with the given ``name``, or ``None`` if no such axis exists."""
for axisDescriptor in self.axes:
if axisDescriptor.name == name:
return axisDescriptor
return None
+ def getLocationLabel(self, name: str) -> Optional[LocationLabelDescriptor]:
+ """Return the top-level location label with the given ``name``, or
+ ``None`` if no such label exists.
+
+ .. versionadded:: 5.0
+ """
+ for label in self.locationLabels:
+ if label.name == name:
+ return label
+ return None
+
+ def map_forward(self, userLocation: SimpleLocationDict) -> SimpleLocationDict:
+ """Map a user location to a design location.
+
+ Assume that missing coordinates are at the default location for that axis.
+
+ Note: the output won't be anisotropic, only the xvalue is set.
+
+ .. versionadded:: 5.0
+ """
+ return {
+ axis.name: axis.map_forward(userLocation.get(axis.name, axis.default))
+ for axis in self.axes
+ }
+
+ def map_backward(self, designLocation: AnisotropicLocationDict) -> SimpleLocationDict:
+ """Map a design location to a user location.
+
+ Assume that missing coordinates are at the default location for that axis.
+
+ When the input has anisotropic locations, only the xvalue is used.
+
+ .. versionadded:: 5.0
+ """
+ return {
+ axis.name: (
+ axis.map_backward(designLocation[axis.name])
+ if axis.name in designLocation
+ else axis.default
+ )
+ for axis in self.axes
+ }
+
def findDefault(self):
"""Set and return SourceDescriptor at the default location or None.
The default location is the set of all `default` values in user space
of all axes.
+
+ This function updates the document's :attr:`default` value.
+
+ .. versionchanged:: 5.0
+ Allow the default source to not specify some of the axis values, and
+ they are assumed to be the default.
+ See :meth:`SourceDescriptor.getFullDesignLocation()`
"""
self.default = None
# Convert the default location from user space to design space before comparing
# it against the SourceDescriptor locations (always in design space).
- default_location_design = self.newDefaultLocation()
+ defaultDesignLocation = self.newDefaultLocation()
for sourceDescriptor in self.sources:
- if sourceDescriptor.location == default_location_design:
+ if sourceDescriptor.getFullDesignLocation(self) == defaultDesignLocation:
self.default = sourceDescriptor
return sourceDescriptor
return None
def normalizeLocation(self, location):
+ """Return a dict with normalized axis values."""
from fontTools.varLib.models import normalizeValue
new = {}
@@ -1317,9 +2787,12 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
return new
def normalize(self):
- # Normalise the geometry of this designspace:
- # scale all the locations of all masters and instances to the -1 - 0 - 1 value.
- # we need the axis data to do the scaling, so we do those last.
+ """
+ Normalise the geometry of this designspace:
+
+ - scale all the locations of all masters and instances to the -1 - 0 - 1 value.
+ - we need the axis data to do the scaling, so we do those last.
+ """
# masters
for item in self.sources:
item.location = self.normalizeLocation(item.location)
@@ -1412,3 +2885,85 @@ class DesignSpaceDocument(LogMixin, AsDictMixin):
loaded[source.path] = source.font
fonts.append(source.font)
return fonts
+
+ @property
+ def formatTuple(self):
+ """Return the formatVersion as a tuple of (major, minor).
+
+ .. versionadded:: 5.0
+ """
+ if self.formatVersion is None:
+ return (5, 0)
+ numbers = (int(i) for i in self.formatVersion.split("."))
+ major = next(numbers)
+ minor = next(numbers, 0)
+ return (major, minor)
+
+ def getVariableFonts(self) -> List[VariableFontDescriptor]:
+ """Return all variable fonts defined in this document, or implicit
+ variable fonts that can be built from the document's continuous axes.
+
+ In the case of Designspace documents before version 5, the whole
+ document was implicitly describing a variable font that covers the
+ whole space.
+
+ In version 5 and above documents, there can be as many variable fonts
+ as there are locations on discrete axes.
+
+ .. seealso:: :func:`splitInterpolable`
+
+ .. versionadded:: 5.0
+ """
+ if self.variableFonts:
+ return self.variableFonts
+
+ variableFonts = []
+ discreteAxes = []
+ rangeAxisSubsets: List[Union[RangeAxisSubsetDescriptor, ValueAxisSubsetDescriptor]] = []
+ for axis in self.axes:
+ 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])
+ for values in valueCombinations:
+ basename = None
+ if self.filename is not None:
+ basename = os.path.splitext(self.filename)[0] + "-VF"
+ if self.path is not None:
+ basename = os.path.splitext(os.path.basename(self.path))[0] + "-VF"
+ if basename is None:
+ basename = "VF"
+ axisNames = "".join([f"-{axis.tag}{value}" for axis, value in zip(discreteAxes, values)])
+ variableFonts.append(VariableFontDescriptor(
+ name=f"{basename}{axisNames}",
+ axisSubsets=rangeAxisSubsets + [
+ ValueAxisSubsetDescriptor(name=axis.name, userValue=value)
+ for axis, value in zip(discreteAxes, values)
+ ]
+ ))
+ return variableFonts
+
+ def deepcopyExceptFonts(self):
+ """Allow deep-copying a DesignSpace document without deep-copying
+ attached UFO fonts or TTFont objects. The :attr:`font` attribute
+ is shared by reference between the original and the copy.
+
+ .. versionadded:: 5.0
+ """
+ fonts = [source.font for source in self.sources]
+ try:
+ for source in self.sources:
+ source.font = None
+ res = copy.deepcopy(self)
+ for source, font in zip(res.sources, fonts):
+ source.font = font
+ return res
+ finally:
+ for source, font in zip(self.sources, fonts):
+ source.font = font
+
diff --git a/Lib/fontTools/designspaceLib/split.py b/Lib/fontTools/designspaceLib/split.py
new file mode 100644
index 00000000..408de70a
--- /dev/null
+++ b/Lib/fontTools/designspaceLib/split.py
@@ -0,0 +1,435 @@
+"""Allows building all the variable fonts of a DesignSpace version 5 by
+splitting the document into interpolable sub-space, then into each VF.
+"""
+
+from __future__ import annotations
+
+import itertools
+import logging
+import math
+from typing import Any, Callable, Dict, Iterator, List, Tuple, cast
+
+from fontTools.designspaceLib import (
+ AxisDescriptor,
+ DesignSpaceDocument,
+ DiscreteAxisDescriptor,
+ InstanceDescriptor,
+ RuleDescriptor,
+ SimpleLocationDict,
+ SourceDescriptor,
+ VariableFontDescriptor,
+)
+from fontTools.designspaceLib.statNames import StatNames, getStatNames
+from fontTools.designspaceLib.types import (
+ ConditionSet,
+ Range,
+ Region,
+ getVFUserRegion,
+ locationInRegion,
+ regionInRegion,
+ userRegionToDesignRegion,
+)
+
+LOGGER = logging.getLogger(__name__)
+
+MakeInstanceFilenameCallable = Callable[
+ [DesignSpaceDocument, InstanceDescriptor, StatNames], str
+]
+
+
+def defaultMakeInstanceFilename(
+ doc: DesignSpaceDocument, instance: InstanceDescriptor, statNames: StatNames
+) -> str:
+ """Default callable to synthesize an instance filename
+ when makeNames=True, for instances that don't specify an instance name
+ in the designspace. This part of the name generation can be overriden
+ because it's not specified by the STAT table.
+ """
+ familyName = instance.familyName or statNames.familyNames.get("en")
+ styleName = instance.styleName or statNames.styleNames.get("en")
+ return f"{familyName}-{styleName}.ttf"
+
+
+def splitInterpolable(
+ doc: DesignSpaceDocument,
+ makeNames: bool = True,
+ expandLocations: bool = True,
+ makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
+) -> Iterator[Tuple[SimpleLocationDict, DesignSpaceDocument]]:
+ """Split the given DS5 into several interpolable sub-designspaces.
+ There are as many interpolable sub-spaces as there are combinations of
+ discrete axis values.
+
+ E.g. with axes:
+ - italic (discrete) Upright or Italic
+ - style (discrete) Sans or Serif
+ - weight (continuous) 100 to 900
+
+ There are 4 sub-spaces in which the Weight axis should interpolate:
+ (Upright, Sans), (Upright, Serif), (Italic, Sans) and (Italic, Serif).
+
+ The sub-designspaces still include the full axis definitions and STAT data,
+ but the rules, sources, variable fonts, instances are trimmed down to only
+ keep what falls within the interpolable sub-space.
+
+ Args:
+ - ``makeNames``: Whether to compute the instance family and style
+ names using the STAT data.
+ - ``expandLocations``: Whether to turn all locations into "full"
+ locations, including implicit default axis values where missing.
+ - ``makeInstanceFilename``: Callable to synthesize an instance filename
+ when makeNames=True, for instances that don't specify an instance name
+ in the designspace. This part of the name generation can be overridden
+ because it's not specified by the STAT table.
+
+ .. versionadded:: 5.0
+ """
+ discreteAxes = []
+ interpolableUserRegion: Region = {}
+ for axis in doc.axes:
+ 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,
+ )
+ valueCombinations = itertools.product(*[axis.values for axis in discreteAxes])
+ for values in valueCombinations:
+ discreteUserLocation = {
+ discreteAxis.name: value
+ for discreteAxis, value in zip(discreteAxes, values)
+ }
+ subDoc = _extractSubSpace(
+ doc,
+ {**interpolableUserRegion, **discreteUserLocation},
+ keepVFs=True,
+ makeNames=makeNames,
+ expandLocations=expandLocations,
+ makeInstanceFilename=makeInstanceFilename,
+ )
+ yield discreteUserLocation, subDoc
+
+
+def splitVariableFonts(
+ doc: DesignSpaceDocument,
+ makeNames: bool = False,
+ expandLocations: bool = False,
+ makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename,
+) -> Iterator[Tuple[str, DesignSpaceDocument]]:
+ """Convert each variable font listed in this document into a standalone
+ designspace. This can be used to compile all the variable fonts from a
+ format 5 designspace using tools that can only deal with 1 VF at a time.
+
+ Args:
+ - ``makeNames``: Whether to compute the instance family and style
+ names using the STAT data.
+ - ``expandLocations``: Whether to turn all locations into "full"
+ locations, including implicit default axis values where missing.
+ - ``makeInstanceFilename``: Callable to synthesize an instance filename
+ when makeNames=True, for instances that don't specify an instance name
+ in the designspace. This part of the name generation can be overridden
+ because it's not specified by the STAT table.
+
+ .. versionadded:: 5.0
+ """
+ # Make one DesignspaceDoc v5 for each variable font
+ for vf in doc.getVariableFonts():
+ vfUserRegion = getVFUserRegion(doc, vf)
+ vfDoc = _extractSubSpace(
+ doc,
+ vfUserRegion,
+ keepVFs=False,
+ makeNames=makeNames,
+ expandLocations=expandLocations,
+ makeInstanceFilename=makeInstanceFilename,
+ )
+ vfDoc.lib = {**vfDoc.lib, **vf.lib}
+ yield vf.name, vfDoc
+
+
+def convert5to4(
+ doc: DesignSpaceDocument,
+) -> Dict[str, DesignSpaceDocument]:
+ """Convert each variable font listed in this document into a standalone
+ format 4 designspace. This can be used to compile all the variable fonts
+ from a format 5 designspace using tools that only know about format 4.
+
+ .. versionadded:: 5.0
+ """
+ vfs = {}
+ for _location, subDoc in splitInterpolable(doc):
+ for vfName, vfDoc in splitVariableFonts(subDoc):
+ vfDoc.formatVersion = "4.1"
+ vfs[vfName] = vfDoc
+ return vfs
+
+
+def _extractSubSpace(
+ doc: DesignSpaceDocument,
+ userRegion: Region,
+ *,
+ keepVFs: bool,
+ makeNames: bool,
+ expandLocations: bool,
+ makeInstanceFilename: MakeInstanceFilenameCallable,
+) -> DesignSpaceDocument:
+ subDoc = DesignSpaceDocument()
+ # Don't include STAT info
+ # FIXME: (Jany) let's think about it. Not include = OK because the point of
+ # the splitting is to build VFs and we'll use the STAT data of the full
+ # document to generate the STAT of the VFs, so "no need" to have STAT data
+ # in sub-docs. Counterpoint: what if someone wants to split this DS for
+ # other purposes? Maybe for that it would be useful to also subset the STAT
+ # data?
+ # subDoc.elidedFallbackName = doc.elidedFallbackName
+
+ def maybeExpandDesignLocation(object):
+ if expandLocations:
+ return object.getFullDesignLocation(doc)
+ else:
+ return object.designLocation
+
+ for axis in doc.axes:
+ range = userRegion[axis.name]
+ 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
+ tag=axis.tag,
+ name=axis.name,
+ labelNames=axis.labelNames,
+ hidden=axis.hidden,
+ # Subset range
+ minimum=max(range.minimum, axis.minimum),
+ default=range.default or axis.default,
+ maximum=min(range.maximum, axis.maximum),
+ map=[
+ (user, design)
+ for user, design in axis.map
+ if range.minimum <= user <= range.maximum
+ ],
+ # Don't include STAT info
+ axisOrdering=None,
+ axisLabels=None,
+ )
+ )
+
+ # Don't include STAT info
+ # subDoc.locationLabels = doc.locationLabels
+
+ # Rules: subset them based on conditions
+ designRegion = userRegionToDesignRegion(doc, userRegion)
+ subDoc.rules = _subsetRulesBasedOnConditions(doc.rules, designRegion)
+ subDoc.rulesProcessingLast = doc.rulesProcessingLast
+
+ # Sources: keep only the ones that fall within the kept axis ranges
+ for source in doc.sources:
+ if not locationInRegion(doc.map_backward(source.designLocation), userRegion):
+ continue
+
+ subDoc.addSource(
+ SourceDescriptor(
+ filename=source.filename,
+ path=source.path,
+ font=source.font,
+ name=source.name,
+ designLocation=_filterLocation(
+ userRegion, maybeExpandDesignLocation(source)
+ ),
+ layerName=source.layerName,
+ familyName=source.familyName,
+ styleName=source.styleName,
+ muteKerning=source.muteKerning,
+ muteInfo=source.muteInfo,
+ mutedGlyphNames=source.mutedGlyphNames,
+ )
+ )
+
+ # Copy family name translations from the old default source to the new default
+ vfDefault = subDoc.findDefault()
+ oldDefault = doc.findDefault()
+ if vfDefault is not None and oldDefault is not None:
+ vfDefault.localisedFamilyName = oldDefault.localisedFamilyName
+
+ # Variable fonts: keep only the ones that fall within the kept axis ranges
+ if keepVFs:
+ # Note: call getVariableFont() to make the implicit VFs explicit
+ for vf in doc.getVariableFonts():
+ vfUserRegion = getVFUserRegion(doc, vf)
+ if regionInRegion(vfUserRegion, userRegion):
+ subDoc.addVariableFont(
+ VariableFontDescriptor(
+ name=vf.name,
+ filename=vf.filename,
+ axisSubsets=[
+ axisSubset
+ for axisSubset in vf.axisSubsets
+ if isinstance(userRegion[axisSubset.name], Range)
+ ],
+ lib=vf.lib,
+ )
+ )
+
+ # Instances: same as Sources + compute missing names
+ for instance in doc.instances:
+ if not locationInRegion(instance.getFullUserLocation(doc), userRegion):
+ continue
+
+ if makeNames:
+ statNames = getStatNames(doc, instance.getFullUserLocation(doc))
+ familyName = instance.familyName or statNames.familyNames.get("en")
+ styleName = instance.styleName or statNames.styleNames.get("en")
+ subDoc.addInstance(
+ InstanceDescriptor(
+ filename=instance.filename
+ or makeInstanceFilename(doc, instance, statNames),
+ path=instance.path,
+ font=instance.font,
+ name=instance.name or f"{familyName} {styleName}",
+ userLocation={} if expandLocations else instance.userLocation,
+ designLocation=_filterLocation(
+ userRegion, maybeExpandDesignLocation(instance)
+ ),
+ familyName=familyName,
+ styleName=styleName,
+ postScriptFontName=instance.postScriptFontName
+ or statNames.postScriptFontName,
+ styleMapFamilyName=instance.styleMapFamilyName
+ or statNames.styleMapFamilyNames.get("en"),
+ styleMapStyleName=instance.styleMapStyleName
+ or statNames.styleMapStyleName,
+ localisedFamilyName=instance.localisedFamilyName
+ or statNames.familyNames,
+ localisedStyleName=instance.localisedStyleName
+ or statNames.styleNames,
+ localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName
+ or statNames.styleMapFamilyNames,
+ localisedStyleMapStyleName=instance.localisedStyleMapStyleName
+ or {},
+ lib=instance.lib,
+ )
+ )
+ else:
+ subDoc.addInstance(
+ InstanceDescriptor(
+ filename=instance.filename,
+ path=instance.path,
+ font=instance.font,
+ name=instance.name,
+ userLocation={} if expandLocations else instance.userLocation,
+ designLocation=_filterLocation(
+ userRegion, maybeExpandDesignLocation(instance)
+ ),
+ familyName=instance.familyName,
+ styleName=instance.styleName,
+ postScriptFontName=instance.postScriptFontName,
+ styleMapFamilyName=instance.styleMapFamilyName,
+ styleMapStyleName=instance.styleMapStyleName,
+ localisedFamilyName=instance.localisedFamilyName,
+ localisedStyleName=instance.localisedStyleName,
+ localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName,
+ localisedStyleMapStyleName=instance.localisedStyleMapStyleName,
+ lib=instance.lib,
+ )
+ )
+
+ subDoc.lib = doc.lib
+
+ return subDoc
+
+
+def _conditionSetFrom(conditionSet: List[Dict[str, Any]]) -> ConditionSet:
+ c: Dict[str, Range] = {}
+ for condition in conditionSet:
+ c[condition["name"]] = Range(
+ condition.get("minimum", -math.inf),
+ condition.get("maximum", math.inf),
+ )
+ return c
+
+
+def _subsetRulesBasedOnConditions(
+ rules: List[RuleDescriptor], designRegion: Region
+) -> List[RuleDescriptor]:
+ # What rules to keep:
+ # - Keep the rule if any conditionset is relevant.
+ # - A conditionset is relevant if all conditions are relevant or it is empty.
+ # - A condition is relevant if
+ # - axis is point (C-AP),
+ # - and point in condition's range (C-AP-in)
+ # (in this case remove the condition because it's always true)
+ # - else (C-AP-out) whole conditionset can be discarded (condition false
+ # => conditionset false)
+ # - axis is range (C-AR),
+ # - (C-AR-all) and axis range fully contained in condition range: we can
+ # scrap the condition because it's always true
+ # - (C-AR-inter) and intersection(axis range, condition range) not empty:
+ # keep the condition with the smaller range (= intersection)
+ # - (C-AR-none) else, whole conditionset can be discarded
+ newRules: List[RuleDescriptor] = []
+ for rule in rules:
+ newRule: RuleDescriptor = RuleDescriptor(
+ name=rule.name, conditionSets=[], subs=rule.subs
+ )
+ for conditionset in rule.conditionSets:
+ cs = _conditionSetFrom(conditionset)
+ newConditionset: List[Dict[str, Any]] = []
+ discardConditionset = False
+ for selectionName, selectionValue in designRegion.items():
+ # TODO: Ensure that all(key in conditionset for key in region.keys())?
+ if selectionName not in cs:
+ # raise Exception("Selection has different axes than the rules")
+ continue
+ if isinstance(selectionValue, (float, int)): # is point
+ # Case C-AP-in
+ if selectionValue in cs[selectionName]:
+ pass # always matches, conditionset can stay empty for this one.
+ # Case C-AP-out
+ else:
+ discardConditionset = True
+ else: # is range
+ # Case C-AR-all
+ if selectionValue in cs[selectionName]:
+ pass # always matches, conditionset can stay empty for this one.
+ else:
+ intersection = cs[selectionName].intersection(selectionValue)
+ # Case C-AR-inter
+ if intersection is not None:
+ newConditionset.append(
+ {
+ "name": selectionName,
+ "minimum": intersection.minimum,
+ "maximum": intersection.maximum,
+ }
+ )
+ # Case C-AR-none
+ else:
+ discardConditionset = True
+ if not discardConditionset:
+ newRule.conditionSets.append(newConditionset)
+ if newRule.conditionSets:
+ newRules.append(newRule)
+
+ return newRules
+
+
+def _filterLocation(
+ userRegion: Region,
+ location: Dict[str, float],
+) -> Dict[str, float]:
+ return {
+ name: value
+ for name, value in location.items()
+ if name in userRegion and isinstance(userRegion[name], Range)
+ }
diff --git a/Lib/fontTools/designspaceLib/statNames.py b/Lib/fontTools/designspaceLib/statNames.py
new file mode 100644
index 00000000..1b672703
--- /dev/null
+++ b/Lib/fontTools/designspaceLib/statNames.py
@@ -0,0 +1,233 @@
+"""Compute name information for a given location in user-space coordinates
+using STAT data. This can be used to fill-in automatically the names of an
+instance:
+
+.. code:: python
+
+ instance = doc.instances[0]
+ names = getStatNames(doc, instance.getFullUserLocation(doc))
+ print(names.styleNames)
+"""
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Dict, Optional, Tuple, Union
+import logging
+
+from fontTools.designspaceLib import (
+ AxisDescriptor,
+ AxisLabelDescriptor,
+ DesignSpaceDocument,
+ DesignSpaceDocumentError,
+ DiscreteAxisDescriptor,
+ SimpleLocationDict,
+ SourceDescriptor,
+)
+
+LOGGER = logging.getLogger(__name__)
+
+# TODO(Python 3.8): use Literal
+# RibbiStyleName = Union[Literal["regular"], Literal["bold"], Literal["italic"], Literal["bold italic"]]
+RibbiStyle = str
+BOLD_ITALIC_TO_RIBBI_STYLE = {
+ (False, False): "regular",
+ (False, True): "italic",
+ (True, False): "bold",
+ (True, True): "bold italic",
+}
+
+
+@dataclass
+class StatNames:
+ """Name data generated from the STAT table information."""
+
+ familyNames: Dict[str, str]
+ styleNames: Dict[str, str]
+ postScriptFontName: Optional[str]
+ styleMapFamilyNames: Dict[str, str]
+ styleMapStyleName: Optional[RibbiStyle]
+
+
+
+def getStatNames(
+ doc: DesignSpaceDocument, userLocation: SimpleLocationDict
+) -> StatNames:
+ """Compute the family, style, PostScript names of the given ``userLocation``
+ using the document's STAT information.
+
+ Also computes localizations.
+
+ If not enough STAT data is available for a given name, either its dict of
+ localized names will be empty (family and style names), or the name will be
+ None (PostScript name).
+
+ .. versionadded:: 5.0
+ """
+ familyNames: Dict[str, str] = {}
+ defaultSource: Optional[SourceDescriptor] = doc.findDefault()
+ if defaultSource is None:
+ LOGGER.warning("Cannot determine default source to look up family name.")
+ elif defaultSource.familyName is None:
+ LOGGER.warning(
+ "Cannot look up family name, assign the 'familyname' attribute to the default source."
+ )
+ else:
+ familyNames = {
+ "en": defaultSource.familyName,
+ **defaultSource.localisedFamilyName,
+ }
+
+ styleNames: Dict[str, str] = {}
+ # If a free-standing label matches the location, use it for name generation.
+ label = doc.labelForUserLocation(userLocation)
+ if label is not None:
+ styleNames = {"en": label.name, **label.labelNames}
+ # Otherwise, scour the axis labels for matches.
+ else:
+ # Gather all languages in which at least one translation is provided
+ # Then build names for all these languages, but fallback to English
+ # whenever a translation is missing.
+ labels = _getAxisLabelsForUserLocation(doc.axes, userLocation)
+ 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 = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")
+
+ styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation)
+
+ styleNamesForStyleMap = styleNames
+ if regularUserLocation != userLocation:
+ regularStatNames = getStatNames(doc, regularUserLocation)
+ styleNamesForStyleMap = regularStatNames.styleNames
+
+ styleMapFamilyNames = {}
+ for language in set(familyNames).union(styleNames.keys()):
+ familyName = familyNames.get(language, familyNames["en"])
+ styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"])
+ styleMapFamilyNames[language] = (familyName + " " + styleName).strip()
+
+ return StatNames(
+ familyNames=familyNames,
+ styleNames=styleNames,
+ postScriptFontName=postScriptFontName,
+ styleMapFamilyNames=styleMapFamilyNames,
+ styleMapStyleName=styleMapStyleName,
+ )
+
+
+def _getSortedAxisLabels(
+ axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
+) -> Dict[str, list[AxisLabelDescriptor]]:
+ """Returns axis labels sorted by their ordering, with unordered ones appended as
+ they are listed."""
+
+ # First, get the axis labels with explicit ordering...
+ sortedAxes = sorted(
+ (axis for axis in axes if axis.axisOrdering is not None),
+ key=lambda a: a.axisOrdering,
+ )
+ sortedLabels: Dict[str, list[AxisLabelDescriptor]] = {
+ axis.name: axis.axisLabels for axis in sortedAxes
+ }
+
+ # ... then append the others in the order they appear.
+ # NOTE: This relies on Python 3.7+ dict's preserved insertion order.
+ for axis in axes:
+ if axis.axisOrdering is None:
+ sortedLabels[axis.name] = axis.axisLabels
+
+ return sortedLabels
+
+
+def _getAxisLabelsForUserLocation(
+ axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
+ userLocation: SimpleLocationDict,
+) -> list[AxisLabelDescriptor]:
+ labels: list[AxisLabelDescriptor] = []
+
+ allAxisLabels = _getSortedAxisLabels(axes)
+ if allAxisLabels.keys() != userLocation.keys():
+ LOGGER.warning(
+ f"Mismatch between user location '{userLocation.keys()}' and available "
+ f"labels for '{allAxisLabels.keys()}'."
+ )
+
+ for axisName, axisLabels in allAxisLabels.items():
+ userValue = userLocation[axisName]
+ label: Optional[AxisLabelDescriptor] = next(
+ (
+ l
+ for l in axisLabels
+ if l.userValue == userValue
+ or (
+ l.userMinimum is not None
+ and l.userMaximum is not None
+ and l.userMinimum <= userValue <= l.userMaximum
+ )
+ ),
+ None,
+ )
+ if label is None:
+ LOGGER.debug(
+ f"Document needs a label for axis '{axisName}', user value '{userValue}'."
+ )
+ else:
+ labels.append(label)
+
+ return labels
+
+
+def _getRibbiStyle(
+ self: DesignSpaceDocument, userLocation: SimpleLocationDict
+) -> Tuple[RibbiStyle, SimpleLocationDict]:
+ """Compute the RIBBI style name of the given user location,
+ return the location of the matching Regular in the RIBBI group.
+
+ .. versionadded:: 5.0
+ """
+ regularUserLocation = {}
+ axes_by_tag = {axis.tag: axis for axis in self.axes}
+
+ bold: bool = False
+ italic: bool = False
+
+ axis = axes_by_tag.get("wght")
+ if axis is not None:
+ for regular_label in axis.axisLabels:
+ if regular_label.linkedUserValue == userLocation[axis.name]:
+ regularUserLocation[axis.name] = regular_label.userValue
+ bold = True
+ break
+
+ axis = axes_by_tag.get("ital") or axes_by_tag.get("slnt")
+ if axis is not None:
+ for urpright_label in axis.axisLabels:
+ if urpright_label.linkedUserValue == userLocation[axis.name]:
+ regularUserLocation[axis.name] = urpright_label.userValue
+ italic = True
+ break
+
+ return BOLD_ITALIC_TO_RIBBI_STYLE[bold, italic], {
+ **userLocation,
+ **regularUserLocation,
+ }
diff --git a/Lib/fontTools/designspaceLib/types.py b/Lib/fontTools/designspaceLib/types.py
new file mode 100644
index 00000000..80ba9d6d
--- /dev/null
+++ b/Lib/fontTools/designspaceLib/types.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Union, cast
+
+from fontTools.designspaceLib import (
+ AxisDescriptor,
+ DesignSpaceDocument,
+ DesignSpaceDocumentError,
+ RangeAxisSubsetDescriptor,
+ SimpleLocationDict,
+ ValueAxisSubsetDescriptor,
+ VariableFontDescriptor,
+)
+
+
+def clamp(value, minimum, maximum):
+ return min(max(value, minimum), maximum)
+
+
+@dataclass
+class Range:
+ minimum: float
+ """Inclusive minimum of the range."""
+ maximum: float
+ """Inclusive maximum of the range."""
+ default: float = 0
+ """Default value"""
+
+ def __post_init__(self):
+ self.minimum, self.maximum = sorted((self.minimum, self.maximum))
+ self.default = clamp(self.default, self.minimum, self.maximum)
+
+ def __contains__(self, value: Union[float, Range]) -> bool:
+ if isinstance(value, Range):
+ return self.minimum <= value.minimum and value.maximum <= self.maximum
+ return self.minimum <= value <= self.maximum
+
+ def intersection(self, other: Range) -> Optional[Range]:
+ if self.maximum < other.minimum or self.minimum > other.maximum:
+ return None
+ else:
+ return Range(
+ max(self.minimum, other.minimum),
+ min(self.maximum, other.maximum),
+ self.default, # We don't care about the default in this use-case
+ )
+
+
+# A region selection is either a range or a single value, as a Designspace v5
+# axis-subset element only allows a single discrete value or a range for a
+# variable-font element.
+Region = Dict[str, Union[Range, float]]
+
+# A conditionset is a set of named ranges.
+ConditionSet = Dict[str, Range]
+
+# A rule is a list of conditionsets where any has to be relevant for the whole rule to be relevant.
+Rule = List[ConditionSet]
+Rules = Dict[str, Rule]
+
+
+def locationInRegion(location: SimpleLocationDict, region: Region) -> bool:
+ for name, value in location.items():
+ if name not in region:
+ return False
+ regionValue = region[name]
+ if isinstance(regionValue, (float, int)):
+ if value != regionValue:
+ return False
+ else:
+ if value not in regionValue:
+ return False
+ return True
+
+
+def regionInRegion(region: Region, superRegion: Region) -> bool:
+ for name, value in region.items():
+ if not name in superRegion:
+ return False
+ superValue = superRegion[name]
+ if isinstance(superValue, (float, int)):
+ if value != superValue:
+ return False
+ else:
+ if value not in superValue:
+ return False
+ return True
+
+
+def userRegionToDesignRegion(doc: DesignSpaceDocument, userRegion: Region) -> Region:
+ 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:
+ designRegion[name] = Range(
+ axis.map_forward(value.minimum),
+ axis.map_forward(value.maximum),
+ axis.map_forward(value.default),
+ )
+ return designRegion
+
+
+def getVFUserRegion(doc: DesignSpaceDocument, vf: VariableFontDescriptor) -> Region:
+ vfUserRegion: Region = {}
+ # For each axis, 2 cases:
+ # - it has a range = it's an axis in the VF DS
+ # - 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 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 bf3b31b7..60382683 100644
--- a/Lib/fontTools/fontBuilder.py
+++ b/Lib/fontTools/fontBuilder.py
@@ -486,15 +486,12 @@ class FontBuilder(object):
"""Create a new `OS/2` table and initialize it with default values,
which can be overridden by keyword arguments.
"""
- if "xAvgCharWidth" not in values:
- gs = self.font.getGlyphSet()
- widths = [
- gs[glyphName].width
- for glyphName in gs.keys()
- if gs[glyphName].width > 0
- ]
- values["xAvgCharWidth"] = int(round(sum(widths) / float(len(widths))))
self._initTableWithValues("OS/2", _OS2Defaults, values)
+ if "xAvgCharWidth" not in values:
+ assert (
+ "hmtx" in self.font
+ ), "the 'hmtx' table must be setup before the 'OS/2' table"
+ self.font["OS/2"].recalcAvgCharWidth(self.font)
if not (
"ulUnicodeRange1" in values
or "ulUnicodeRange2" in values
@@ -841,6 +838,7 @@ class FontBuilder(object):
varStore=None,
varIndexMap=None,
clipBoxes=None,
+ allowLayerReuse=True,
):
"""Build new COLR table using color layers dictionary.
@@ -856,6 +854,7 @@ class FontBuilder(object):
varStore=varStore,
varIndexMap=varIndexMap,
clipBoxes=clipBoxes,
+ allowLayerReuse=allowLayerReuse,
)
def setupCPAL(
diff --git a/Lib/fontTools/merge/__init__.py b/Lib/fontTools/merge/__init__.py
index 152bf079..97106489 100644
--- a/Lib/fontTools/merge/__init__.py
+++ b/Lib/fontTools/merge/__init__.py
@@ -155,6 +155,11 @@ class Merger(object):
def _postMerge(self, font):
layoutPostMerge(font)
+ if "OS/2" in font:
+ # https://github.com/fonttools/fonttools/issues/2538
+ # TODO: Add an option to disable this?
+ font["OS/2"].recalcAvgCharWidth(font)
+
__all__ = [
'Options',
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/merge/tables.py b/Lib/fontTools/merge/tables.py
index b266f7a9..ac6d59b5 100644
--- a/Lib/fontTools/merge/tables.py
+++ b/Lib/fontTools/merge/tables.py
@@ -132,7 +132,7 @@ ttLib.getTableClass('OS/2').mergeMap = {
'*': first,
'tableTag': equal,
'version': max,
- 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
+ 'xAvgCharWidth': first, # Will be recalculated at the end on the merged font
'fsType': mergeOs2FsType, # Will be overwritten
'panose': first, # FIXME: should really be the first Latin font
'ulUnicodeRange1': bitwise_or,
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/configTools.py b/Lib/fontTools/misc/configTools.py
new file mode 100644
index 00000000..38bbada2
--- /dev/null
+++ b/Lib/fontTools/misc/configTools.py
@@ -0,0 +1,348 @@
+"""
+Code of the config system; not related to fontTools or fonts in particular.
+
+The options that are specific to fontTools are in :mod:`fontTools.config`.
+
+To create your own config system, you need to create an instance of
+:class:`Options`, and a subclass of :class:`AbstractConfig` with its
+``options`` class variable set to your instance of Options.
+
+"""
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass
+from typing import (
+ Any,
+ Callable,
+ ClassVar,
+ Dict,
+ Iterable,
+ Mapping,
+ MutableMapping,
+ Optional,
+ Set,
+ Union,
+)
+
+
+log = logging.getLogger(__name__)
+
+__all__ = [
+ "AbstractConfig",
+ "ConfigAlreadyRegisteredError",
+ "ConfigError",
+ "ConfigUnknownOptionError",
+ "ConfigValueParsingError",
+ "ConfigValueValidationError",
+ "Option",
+ "Options",
+]
+
+
+class ConfigError(Exception):
+ """Base exception for the config module."""
+
+
+class ConfigAlreadyRegisteredError(ConfigError):
+ """Raised when a module tries to register a configuration option that
+ already exists.
+
+ Should not be raised too much really, only when developing new fontTools
+ modules.
+ """
+
+ def __init__(self, name):
+ super().__init__(f"Config option {name} is already registered.")
+
+
+class ConfigValueParsingError(ConfigError):
+ """Raised when a configuration value cannot be parsed."""
+
+ def __init__(self, name, value):
+ super().__init__(
+ f"Config option {name}: value cannot be parsed (given {repr(value)})"
+ )
+
+
+class ConfigValueValidationError(ConfigError):
+ """Raised when a configuration value cannot be validated."""
+
+ def __init__(self, name, value):
+ super().__init__(
+ f"Config option {name}: value is invalid (given {repr(value)})"
+ )
+
+
+class ConfigUnknownOptionError(ConfigError):
+ """Raised when a configuration option is unknown."""
+
+ def __init__(self, option_or_name):
+ name = (
+ f"'{option_or_name.name}' (id={id(option_or_name)})>"
+ if isinstance(option_or_name, Option)
+ else f"'{option_or_name}'"
+ )
+ super().__init__(f"Config option {name} is unknown")
+
+
+# eq=False because Options are unique, not fungible objects
+@dataclass(frozen=True, eq=False)
+class Option:
+ name: str
+ """Unique name identifying the option (e.g. package.module:MY_OPTION)."""
+ help: str
+ """Help text for this option."""
+ default: Any
+ """Default value for this option."""
+ parse: Callable[[str], Any]
+ """Turn input (e.g. string) into proper type. Only when reading from file."""
+ validate: Optional[Callable[[Any], bool]] = None
+ """Return true if the given value is an acceptable value."""
+
+ @staticmethod
+ def parse_optional_bool(v: str) -> Optional[bool]:
+ s = str(v).lower()
+ if s in {"0", "no", "false"}:
+ return False
+ if s in {"1", "yes", "true"}:
+ return True
+ if s in {"auto", "none"}:
+ return None
+ raise ValueError("invalid optional bool: {v!r}")
+
+ @staticmethod
+ def validate_optional_bool(v: Any) -> bool:
+ return v is None or isinstance(v, bool)
+
+
+class Options(Mapping):
+ """Registry of available options for a given config system.
+
+ Define new options using the :meth:`register()` method.
+
+ Access existing options using the Mapping interface.
+ """
+
+ __options: Dict[str, Option]
+
+ def __init__(self, other: "Options" = None) -> None:
+ self.__options = {}
+ if other is not None:
+ for option in other.values():
+ self.register_option(option)
+
+ def register(
+ self,
+ name: str,
+ help: str,
+ default: Any,
+ parse: Callable[[str], Any],
+ validate: Optional[Callable[[Any], bool]] = None,
+ ) -> Option:
+ """Create and register a new option."""
+ return self.register_option(Option(name, help, default, parse, validate))
+
+ def register_option(self, option: Option) -> Option:
+ """Register a new option."""
+ name = option.name
+ if name in self.__options:
+ raise ConfigAlreadyRegisteredError(name)
+ self.__options[name] = option
+ return option
+
+ def is_registered(self, option: Option) -> bool:
+ """Return True if the same option object is already registered."""
+ return self.__options.get(option.name) is option
+
+ def __getitem__(self, key: str) -> Option:
+ return self.__options.__getitem__(key)
+
+ def __iter__(self) -> Iterator[str]:
+ return self.__options.__iter__()
+
+ def __len__(self) -> int:
+ return self.__options.__len__()
+
+ def __repr__(self) -> str:
+ return (
+ f"{self.__class__.__name__}({{\n"
+ + "".join(
+ f" {k!r}: Option(default={v.default!r}, ...),\n"
+ for k, v in self.__options.items()
+ )
+ + "})"
+ )
+
+
+_USE_GLOBAL_DEFAULT = object()
+
+
+class AbstractConfig(MutableMapping):
+ """
+ Create a set of config values, optionally pre-filled with values from
+ the given dictionary or pre-existing config object.
+
+ The class implements the MutableMapping protocol keyed by option name (`str`).
+ For convenience its methods accept either Option or str as the key parameter.
+
+ .. seealso:: :meth:`set()`
+
+ This config class is abstract because it needs its ``options`` class
+ var to be set to an instance of :class:`Options` before it can be
+ instanciated and used.
+
+ .. code:: python
+
+ class MyConfig(AbstractConfig):
+ options = Options()
+
+ MyConfig.register_option( "test:option_name", "This is an option", 0, int, lambda v: isinstance(v, int))
+
+ cfg = MyConfig({"test:option_name": 10})
+
+ """
+
+ options: ClassVar[Options]
+
+ @classmethod
+ def register_option(
+ cls,
+ name: str,
+ help: str,
+ default: Any,
+ parse: Callable[[str], Any],
+ validate: Optional[Callable[[Any], bool]] = None,
+ ) -> Option:
+ """Register an available option in this config system."""
+ return cls.options.register(
+ name, help=help, default=default, parse=parse, validate=validate
+ )
+
+ _values: Dict[str, Any]
+
+ def __init__(
+ self,
+ values: Union[AbstractConfig, Dict[Union[Option, str], Any]] = {},
+ parse_values: bool = False,
+ skip_unknown: bool = False,
+ ):
+ self._values = {}
+ values_dict = values._values if isinstance(values, AbstractConfig) else values
+ for name, value in values_dict.items():
+ self.set(name, value, parse_values, skip_unknown)
+
+ def _resolve_option(self, option_or_name: Union[Option, str]) -> Option:
+ if isinstance(option_or_name, Option):
+ option = option_or_name
+ if not self.options.is_registered(option):
+ raise ConfigUnknownOptionError(option)
+ return option
+ elif isinstance(option_or_name, str):
+ name = option_or_name
+ try:
+ return self.options[name]
+ except KeyError:
+ raise ConfigUnknownOptionError(name)
+ else:
+ raise TypeError(
+ "expected Option or str, found "
+ f"{type(option_or_name).__name__}: {option_or_name!r}"
+ )
+
+ def set(
+ self,
+ option_or_name: Union[Option, str],
+ value: Any,
+ parse_values: bool = False,
+ skip_unknown: bool = False,
+ ):
+ """Set the value of an option.
+
+ Args:
+ * `option_or_name`: an `Option` object or its name (`str`).
+ * `value`: the value to be assigned to given option.
+ * `parse_values`: parse the configuration value from a string into
+ its proper type, as per its `Option` object. The default
+ behavior is to raise `ConfigValueValidationError` when the value
+ is not of the right type. Useful when reading options from a
+ file type that doesn't support as many types as Python.
+ * `skip_unknown`: skip unknown configuration options. The default
+ behaviour is to raise `ConfigUnknownOptionError`. Useful when
+ reading options from a configuration file that has extra entries
+ (e.g. for a later version of fontTools)
+ """
+ try:
+ option = self._resolve_option(option_or_name)
+ except ConfigUnknownOptionError as e:
+ if skip_unknown:
+ log.debug(str(e))
+ return
+ raise
+
+ # Can be useful if the values come from a source that doesn't have
+ # strict typing (.ini file? Terminal input?)
+ if parse_values:
+ try:
+ value = option.parse(value)
+ except Exception as e:
+ raise ConfigValueParsingError(option.name, value) from e
+
+ if option.validate is not None and not option.validate(value):
+ raise ConfigValueValidationError(option.name, value)
+
+ self._values[option.name] = value
+
+ def get(
+ self, option_or_name: Union[Option, str], default: Any = _USE_GLOBAL_DEFAULT
+ ) -> Any:
+ """
+ Get the value of an option. The value which is returned is the first
+ provided among:
+
+ 1. a user-provided value in the options's ``self._values`` dict
+ 2. a caller-provided default value to this method call
+ 3. the global default for the option provided in ``fontTools.config``
+
+ This is to provide the ability to migrate progressively from config
+ options passed as arguments to fontTools APIs to config options read
+ from the current TTFont, e.g.
+
+ .. code:: python
+
+ def fontToolsAPI(font, some_option):
+ value = font.cfg.get("someLib.module:SOME_OPTION", some_option)
+ # use value
+
+ That way, the function will work the same for users of the API that
+ still pass the option to the function call, but will favour the new
+ config mechanism if the given font specifies a value for that option.
+ """
+ option = self._resolve_option(option_or_name)
+ if option.name in self._values:
+ return self._values[option.name]
+ if default is not _USE_GLOBAL_DEFAULT:
+ return default
+ return option.default
+
+ def copy(self):
+ return self.__class__(self._values)
+
+ def __getitem__(self, option_or_name: Union[Option, str]) -> Any:
+ return self.get(option_or_name)
+
+ def __setitem__(self, option_or_name: Union[Option, str], value: Any) -> None:
+ return self.set(option_or_name, value)
+
+ def __delitem__(self, option_or_name: Union[Option, str]) -> None:
+ option = self._resolve_option(option_or_name)
+ del self._values[option.name]
+
+ def __iter__(self) -> Iterable[str]:
+ return self._values.__iter__()
+
+ def __len__(self) -> int:
+ return len(self._values)
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}({repr(self._values)})"
diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py
index 29c2d365..549dae25 100644
--- a/Lib/fontTools/misc/psCharStrings.py
+++ b/Lib/fontTools/misc/psCharStrings.py
@@ -502,11 +502,20 @@ class T2OutlineExtractor(T2WidthExtractor):
T2WidthExtractor.__init__(
self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private)
self.pen = pen
+ self.subrLevel = 0
def reset(self):
T2WidthExtractor.reset(self)
self.currentPoint = (0, 0)
self.sawMoveTo = 0
+ self.subrLevel = 0
+
+ def execute(self, charString):
+ self.subrLevel += 1
+ super().execute(charString)
+ self.subrLevel -= 1
+ if self.subrLevel == 0:
+ self.endPath()
def _nextPoint(self, point):
x, y = self.currentPoint
@@ -536,8 +545,11 @@ class T2OutlineExtractor(T2WidthExtractor):
def endPath(self):
# In T2 there are no open paths, so always do a closePath when
- # finishing a sub path.
- self.closePath()
+ # finishing a sub path. We avoid spurious calls to closePath()
+ # because its a real T1 op we're emulating in T2 whereas
+ # endPath() is just a means to that emulation
+ if self.sawMoveTo:
+ self.closePath()
#
# hint operators
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/testTools.py b/Lib/fontTools/misc/testTools.py
index db316a82..871a9951 100644
--- a/Lib/fontTools/misc/testTools.py
+++ b/Lib/fontTools/misc/testTools.py
@@ -3,10 +3,12 @@
from collections.abc import Iterable
from io import BytesIO
import os
+import re
import shutil
import sys
import tempfile
from unittest import TestCase as _TestCase
+from fontTools.config import Config
from fontTools.misc.textTools import tobytes
from fontTools.misc.xmlWriter import XMLWriter
@@ -52,6 +54,7 @@ class FakeFont:
self.reverseGlyphOrderDict_ = {g: i for i, g in enumerate(glyphs)}
self.lazy = False
self.tables = {}
+ self.cfg = Config()
def __getitem__(self, tag):
return self.tables[tag]
@@ -133,6 +136,31 @@ def getXML(func, ttFont=None):
return xml.splitlines()
+def stripVariableItemsFromTTX(
+ string: str,
+ ttLibVersion: bool = True,
+ checkSumAdjustment: bool = True,
+ modified: bool = True,
+ created: bool = True,
+ sfntVersion: bool = False, # opt-in only
+) -> str:
+ """Strip stuff like ttLibVersion, checksums, timestamps, etc. from TTX dumps."""
+ # ttlib changes with the fontTools version
+ if ttLibVersion:
+ string = re.sub(' ttLibVersion="[^"]+"', "", string)
+ # sometimes (e.g. some subsetter tests) we don't care whether it's OTF or TTF
+ if sfntVersion:
+ string = re.sub(' sfntVersion="[^"]+"', "", string)
+ # head table checksum and creation and mod date changes with each save.
+ if checkSumAdjustment:
+ string = re.sub('<checkSumAdjustment value="[^"]+"/>', "", string)
+ if modified:
+ string = re.sub('<modified value="[^"]+"/>', "", string)
+ if created:
+ string = re.sub('<created value="[^"]+"/>', "", string)
+ return string
+
+
class MockFont(object):
"""A font-like object that automatically adds any looked up glyphname
to its glyphOrder."""
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/builder.py b/Lib/fontTools/otlLib/builder.py
index e3f33551..233edec2 100644
--- a/Lib/fontTools/otlLib/builder.py
+++ b/Lib/fontTools/otlLib/builder.py
@@ -12,8 +12,7 @@ from fontTools.ttLib.tables.otBase import (
from fontTools.ttLib.tables import otBase
from fontTools.feaLib.ast import STATNameStatement
from fontTools.otlLib.optimize.gpos import (
- GPOS_COMPACT_MODE_DEFAULT,
- GPOS_COMPACT_MODE_ENV_KEY,
+ _compression_level_from_env,
compact_lookup,
)
from fontTools.otlLib.error import OpenTypeLibError
@@ -367,9 +366,15 @@ class ChainContextualBuilder(LookupBuilder):
contextual positioning lookup.
"""
subtables = []
- chaining = False
+
rulesets = self.rulesets()
chaining = any(ruleset.hasPrefixOrSuffix for ruleset in rulesets)
+ # Unfortunately, as of 2022-03-07, Apple's CoreText renderer does not
+ # correctly process GPOS7 lookups, so for now we force contextual
+ # positioning lookups to be chaining (GPOS8).
+ if self.subtable_type == "Pos": # horrible separation of concerns breach
+ chaining = True
+
for ruleset in rulesets:
# Determine format strategy. We try to build formats 1, 2 and 3
# subtables and then work out which is best. candidates list holds
@@ -1408,10 +1413,14 @@ class PairPosBuilder(LookupBuilder):
# Compact the lookup
# This is a good moment to do it because the compaction should create
# smaller subtables, which may prevent overflows from happening.
- mode = os.environ.get(GPOS_COMPACT_MODE_ENV_KEY, GPOS_COMPACT_MODE_DEFAULT)
- if mode and mode != "0":
+ # Keep reading the value from the ENV until ufo2ft switches to the config system
+ level = self.font.cfg.get(
+ "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
+ default=_compression_level_from_env(),
+ )
+ if level != 0:
log.info("Compacting GPOS...")
- compact_lookup(self.font, mode, lookup)
+ compact_lookup(self.font, level, lookup)
return lookup
diff --git a/Lib/fontTools/otlLib/optimize/__init__.py b/Lib/fontTools/otlLib/optimize/__init__.py
index 5c007e89..25bce9cd 100644
--- a/Lib/fontTools/otlLib/optimize/__init__.py
+++ b/Lib/fontTools/otlLib/optimize/__init__.py
@@ -1,39 +1,27 @@
from argparse import RawTextHelpFormatter
-from textwrap import dedent
-
+from fontTools.otlLib.optimize.gpos import COMPRESSION_LEVEL, compact
from fontTools.ttLib import TTFont
-from fontTools.otlLib.optimize.gpos import compact, GPOS_COMPACT_MODE_DEFAULT
+
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
- parser = ArgumentParser(prog="otlLib.optimize", description=main.__doc__, formatter_class=RawTextHelpFormatter)
+ parser = ArgumentParser(
+ prog="otlLib.optimize",
+ description=main.__doc__,
+ formatter_class=RawTextHelpFormatter,
+ )
parser.add_argument("font")
parser.add_argument(
"-o", metavar="OUTPUTFILE", dest="outfile", default=None, help="output file"
)
parser.add_argument(
- "--gpos-compact-mode",
- help=dedent(
- f"""\
- GPOS Lookup type 2 (PairPos) compaction mode:
- 0 = do not attempt to compact PairPos lookups;
- 1 to 8 = create at most 1 to 8 new subtables for each existing
- subtable, provided that it would yield a 50%% file size saving;
- 9 = create as many new subtables as needed to yield a file size saving.
- Default: {GPOS_COMPACT_MODE_DEFAULT}.
-
- This compaction aims to save file size, by splitting large class
- kerning subtables (Format 2) that contain many zero values into
- smaller and denser subtables. It's a trade-off between the overhead
- of several subtables versus the sparseness of one big subtable.
-
- See the pull request: https://github.com/fonttools/fonttools/pull/2326
- """
- ),
- default=int(GPOS_COMPACT_MODE_DEFAULT),
+ "--gpos-compression-level",
+ help=COMPRESSION_LEVEL.help,
+ default=COMPRESSION_LEVEL.default,
choices=list(range(10)),
type=int,
)
@@ -51,12 +39,10 @@ def main(args=None):
)
font = TTFont(options.font)
- # TODO: switch everything to have type(mode) = int when using the Config class
- compact(font, str(options.gpos_compact_mode))
+ compact(font, options.gpos_compression_level)
font.save(options.outfile or options.font)
-
if __name__ == "__main__":
import sys
@@ -65,4 +51,3 @@ if __name__ == "__main__":
import doctest
sys.exit(doctest.testmod().failed)
-
diff --git a/Lib/fontTools/otlLib/optimize/gpos.py b/Lib/fontTools/otlLib/optimize/gpos.py
index 79873fad..0acd9ed0 100644
--- a/Lib/fontTools/otlLib/optimize/gpos.py
+++ b/Lib/fontTools/otlLib/optimize/gpos.py
@@ -1,24 +1,45 @@
import logging
+import os
from collections import defaultdict, namedtuple
from functools import reduce
from itertools import chain
from math import log2
from typing import DefaultDict, Dict, Iterable, List, Sequence, Tuple
+from fontTools.config import OPTIONS
from fontTools.misc.intTools import bit_count, bit_indices
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables import otBase, otTables
-# NOTE: activating this optimization via the environment variable is
-# experimental and may not be supported once an alternative mechanism
-# is in place. See: https://github.com/fonttools/fonttools/issues/2349
+log = logging.getLogger(__name__)
+
+COMPRESSION_LEVEL = OPTIONS[f"{__name__}:COMPRESSION_LEVEL"]
+
+# Kept because ufo2ft depends on it, to be removed once ufo2ft uses the config instead
+# https://github.com/fonttools/fonttools/issues/2592
GPOS_COMPACT_MODE_ENV_KEY = "FONTTOOLS_GPOS_COMPACT_MODE"
-GPOS_COMPACT_MODE_DEFAULT = "0"
+GPOS_COMPACT_MODE_DEFAULT = str(COMPRESSION_LEVEL.default)
+
-log = logging.getLogger("fontTools.otlLib.optimize.gpos")
+def _compression_level_from_env() -> int:
+ env_level = GPOS_COMPACT_MODE_DEFAULT
+ if GPOS_COMPACT_MODE_ENV_KEY in os.environ:
+ import warnings
+
+ warnings.warn(
+ f"'{GPOS_COMPACT_MODE_ENV_KEY}' environment variable is deprecated. "
+ "Please set the 'fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL' option "
+ "in TTFont.cfg.",
+ DeprecationWarning,
+ )
+ env_level = os.environ[GPOS_COMPACT_MODE_ENV_KEY]
+ if len(env_level) == 1 and env_level in "0123456789":
+ return int(env_level)
+ raise ValueError(f"Bad {GPOS_COMPACT_MODE_ENV_KEY}={env_level}")
-def compact(font: TTFont, mode: str) -> TTFont:
+
+def compact(font: TTFont, level: int) -> TTFont:
# Ideal plan:
# 1. Find lookups of Lookup Type 2: Pair Adjustment Positioning Subtable
# https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#lookup-type-2-pair-adjustment-positioning-subtable
@@ -35,21 +56,21 @@ def compact(font: TTFont, mode: str) -> TTFont:
gpos = font["GPOS"]
for lookup in gpos.table.LookupList.Lookup:
if lookup.LookupType == 2:
- compact_lookup(font, mode, lookup)
+ compact_lookup(font, level, lookup)
elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2:
- compact_ext_lookup(font, mode, lookup)
+ compact_ext_lookup(font, level, lookup)
return font
-def compact_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None:
- new_subtables = compact_pair_pos(font, mode, lookup.SubTable)
+def compact_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
+ new_subtables = compact_pair_pos(font, level, lookup.SubTable)
lookup.SubTable = new_subtables
lookup.SubTableCount = len(new_subtables)
-def compact_ext_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None:
+def compact_ext_lookup(font: TTFont, level: int, lookup: otTables.Lookup) -> None:
new_subtables = compact_pair_pos(
- font, mode, [ext_subtable.ExtSubTable for ext_subtable in lookup.SubTable]
+ font, level, [ext_subtable.ExtSubTable for ext_subtable in lookup.SubTable]
)
new_ext_subtables = []
for subtable in new_subtables:
@@ -62,7 +83,7 @@ def compact_ext_lookup(font: TTFont, mode: str, lookup: otTables.Lookup) -> None
def compact_pair_pos(
- font: TTFont, mode: str, subtables: Sequence[otTables.PairPos]
+ font: TTFont, level: int, subtables: Sequence[otTables.PairPos]
) -> Sequence[otTables.PairPos]:
new_subtables = []
for subtable in subtables:
@@ -70,12 +91,12 @@ def compact_pair_pos(
# Not doing anything to Format 1 (yet?)
new_subtables.append(subtable)
elif subtable.Format == 2:
- new_subtables.extend(compact_class_pairs(font, mode, subtable))
+ new_subtables.extend(compact_class_pairs(font, level, subtable))
return new_subtables
def compact_class_pairs(
- font: TTFont, mode: str, subtable: otTables.PairPos
+ font: TTFont, level: int, subtable: otTables.PairPos
) -> List[otTables.PairPos]:
from fontTools.otlLib.builder import buildPairPosClassesSubtable
@@ -95,17 +116,9 @@ def compact_class_pairs(
getattr(class2, "Value1", None),
getattr(class2, "Value2", None),
)
-
- if len(mode) == 1 and mode in "123456789":
- grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(
- font, all_pairs, int(mode)
- )
- for pairs in grouped_pairs:
- subtables.append(
- buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap())
- )
- else:
- raise ValueError(f"Bad {GPOS_COMPACT_MODE_ENV_KEY}={mode}")
+ grouped_pairs = cluster_pairs_by_class2_coverage_custom_cost(font, all_pairs, level)
+ for pairs in grouped_pairs:
+ subtables.append(buildPairPosClassesSubtable(pairs, font.getReverseGlyphMap()))
return subtables
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 53b440da..b58e6162 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -2,15 +2,19 @@
#
# Google Author(s): Behdad Esfahbod
+from fontTools import config
from fontTools.misc.roundTools import otRound
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
+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
@@ -144,6 +148,15 @@ Output options
The Zopfli Python bindings are available at:
https://pypi.python.org/pypi/zopfli
+--harfbuzz-repacker
+ By default, we serialize GPOS/GSUB using the HarfBuzz Repacker when
+ uharfbuzz can be imported and is successful, otherwise fall back to
+ the pure-python serializer. Set the option to force using the HarfBuzz
+ Repacker (raises an error if uharfbuzz can't be found or fails).
+
+--no-harfbuzz-repacker
+ Always use the pure-python serializer even if uharfbuzz is available.
+
Glyph set expansion
^^^^^^^^^^^^^^^^^^^
@@ -625,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):
@@ -2597,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'],
@@ -2642,6 +2664,7 @@ class Options(object):
self.flavor = None # May be 'woff' or 'woff2'
self.with_zopfli = False # use zopfli instead of zlib for WOFF 1.0
self.desubroutinize = False # Desubroutinize CFF CharStrings
+ self.harfbuzz_repacker = USE_HARFBUZZ_REPACKER.default
self.verbose = False
self.timing = False
self.xml = False
@@ -2964,11 +2987,10 @@ class Subsetter(object):
if old_uniranges != new_uniranges:
log.info("%s Unicode ranges pruned: %s", tag, sorted(new_uniranges))
if self.options.recalc_average_width:
- widths = [m[0] for m in font["hmtx"].metrics.values() if m[0] > 0]
- avg_width = otRound(sum(widths) / len(widths))
- if avg_width != font[tag].xAvgCharWidth:
- font[tag].xAvgCharWidth = avg_width
- log.info("%s xAvgCharWidth updated: %d", tag, avg_width)
+ old_avg_width = font[tag].xAvgCharWidth
+ new_avg_width = font[tag].recalcAvgCharWidth(font)
+ if old_avg_width != new_avg_width:
+ log.info("%s xAvgCharWidth updated: %d", tag, new_avg_width)
if self.options.recalc_max_context:
max_context = maxCtxFont(font)
if max_context != font[tag].usMaxContext:
@@ -3038,6 +3060,7 @@ def save_font(font, outfile, options):
from fontTools.ttLib import sfnt
sfnt.USE_ZOPFLI = True
font.flavor = options.flavor
+ font.cfg[USE_HARFBUZZ_REPACKER] = options.harfbuzz_repacker
font.save(outfile, reorderTables=options.canonical_order)
def parse_unicodes(s):
@@ -3168,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/O_S_2f_2.py b/Lib/fontTools/ttLib/tables/O_S_2f_2.py
index a5765224..ba2e3961 100644
--- a/Lib/fontTools/ttLib/tables/O_S_2f_2.py
+++ b/Lib/fontTools/ttLib/tables/O_S_2f_2.py
@@ -1,4 +1,5 @@
from fontTools.misc import sstruct
+from fontTools.misc.roundTools import otRound
from fontTools.misc.textTools import safeEval, num2binary, binary2num
from fontTools.ttLib.tables import DefaultTable
import bisect
@@ -299,6 +300,19 @@ class table_O_S_2f_2(DefaultTable.DefaultTable):
self.setUnicodeRanges(bits)
return bits
+ def recalcAvgCharWidth(self, ttFont):
+ """Recalculate xAvgCharWidth using metrics from ttFont's 'hmtx' table.
+
+ Set it to 0 if the unlikely event 'hmtx' table is not found.
+ """
+ avg_width = 0
+ hmtx = ttFont.get("hmtx")
+ if hmtx:
+ widths = [m[0] for m in hmtx.metrics.values() if m[0] > 0]
+ avg_width = otRound(sum(widths) / len(widths))
+ self.xAvgCharWidth = avg_width
+ return avg_width
+
# Unicode ranges data from the OpenType OS/2 table specification v1.7
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 a31b5059..ef2b5758 100644
--- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py
@@ -91,6 +91,11 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
(0, 1), # Unicode 1.1
(0, 0) # Unicode 1.0
+ This particular order matches what HarfBuzz uses to choose what
+ subtable to use by default. This order prefers the largest-repertoire
+ subtable, and among those, prefers the Windows-platform over the
+ Unicode-platform as the former has wider support.
+
This order can be customized via the ``cmapPreferences`` argument.
"""
for platformID, platEncID in cmapPreferences:
@@ -159,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()
@@ -172,13 +179,11 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
seen = {} # Some tables are the same object reference. Don't compile them twice.
done = {} # Some tables are different objects, but compile to the same data chunk
for table in self.tables:
- try:
- offset = seen[id(table.cmap)]
- except KeyError:
+ offset = seen.get(id(table.cmap))
+ if offset is None:
chunk = table.compile(ttFont)
- if chunk in done:
- offset = done[chunk]
- else:
+ offset = done.get(chunk)
+ if offset is None:
offset = seen[id(table.cmap)] = done[chunk] = totalOffset + len(tableData)
tableData = tableData + chunk
data = data + struct.pack(">HHl", table.platformID, table.platEncID, offset)
@@ -238,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.
@@ -800,7 +807,6 @@ class cmap_format_4(CmapSubtable):
start = startCode[i]
delta = idDelta[i]
rangeOffset = idRangeOffset[i]
- # *someone* needs to get killed.
partial = rangeOffset // 2 - start + i - len(idRangeOffset)
rangeCharCodes = list(range(startCode[i], endCode[i] + 1))
@@ -891,7 +897,6 @@ class cmap_format_4(CmapSubtable):
idDelta.append((indices[0] - startCode[i]) % 0x10000)
idRangeOffset.append(0)
else:
- # someone *definitely* needs to get killed.
idDelta.append(0)
idRangeOffset.append(2 * (len(endCode) + len(glyphIndexArray) - i))
glyphIndexArray.extend(indices)
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 bc2c9fba..1bd3198d 100644
--- a/Lib/fontTools/ttLib/tables/otBase.py
+++ b/Lib/fontTools/ttLib/tables/otBase.py
@@ -1,13 +1,28 @@
+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
+ # 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
+
+USE_HARFBUZZ_REPACKER = OPTIONS[f"{__name__}:USE_HARFBUZZ_REPACKER"]
+
class OverflowErrorRecord(object):
def __init__(self, overflowTuple):
self.tableType = overflowTuple[0]
@@ -26,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):
@@ -66,37 +100,118 @@ class BaseTTXConverter(DefaultTable):
# If a lookup subtable overflows an offset, we have to start all over.
overflowRecord = None
+ # this is 3-state option: default (None) means automatically use hb.repack or
+ # silently fall back if it fails; True, use it and raise error if not possible
+ # or it errors out; False, don't use it, even if you can.
+ use_hb_repack = font.cfg[USE_HARFBUZZ_REPACKER]
+ if self.tableTag in ("GSUB", "GPOS"):
+ if use_hb_repack is False:
+ log.debug(
+ "hb.repack disabled, compiling '%s' with pure-python serializer",
+ self.tableTag,
+ )
+ elif not have_uharfbuzz:
+ if use_hb_repack is True:
+ raise ImportError("No module named 'uharfbuzz'")
+ else:
+ assert use_hb_repack is None
+ log.debug(
+ "uharfbuzz not found, compiling '%s' with pure-python serializer",
+ 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)
- 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)
@@ -109,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
@@ -298,6 +413,20 @@ class OTTableWriter(object):
return bytesjoin(items)
+ def getDataForHarfbuzz(self):
+ """Assemble the data for this writer/table with all offset field set to 0"""
+ items = list(self.items)
+ packFuncs = {2: packUShort, 3: packUInt24, 4: packULong}
+ for i, item in enumerate(items):
+ if hasattr(item, "getData"):
+ # Offset value is not needed in harfbuzz repacker, so setting offset to 0 to avoid overflow here
+ if item.offsetSize in packFuncs:
+ items[i] = packFuncs[item.offsetSize](0)
+ else:
+ raise ValueError(item.offsetSize)
+
+ return bytesjoin(items)
+
def __hash__(self):
# only works after self._doneWriting() has been called
return hash(self.items)
@@ -311,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.
@@ -327,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
@@ -336,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
@@ -402,10 +531,97 @@ class OTTableWriter(object):
selfTables.append(self)
- def getAllData(self):
- """Assemble all data, including all subtables."""
+ def _gatherGraphForHarfbuzz(self, tables, obj_list, done, objidx, virtual_edges):
+ real_links = []
+ virtual_links = []
+ item_idx = objidx
+
+ # Merge virtual_links from parent
+ for idx in virtual_edges:
+ virtual_links.append((0, 0, idx))
+
+ sortCoverageLast = False
+ coverage_idx = 0
+ if hasattr(self, "sortCoverageLast"):
+ # Find coverage table
+ for i, item in enumerate(self.items):
+ if getattr(item, 'name', None) == "Coverage":
+ sortCoverageLast = True
+ if id(item) not in done:
+ coverage_idx = item_idx = item._gatherGraphForHarfbuzz(tables, obj_list, done, item_idx, virtual_edges)
+ else:
+ coverage_idx = done[id(item)]
+ virtual_edges.append(coverage_idx)
+ break
+
+ child_idx = 0
+ offset_pos = 0
+ for i, item in enumerate(self.items):
+ if hasattr(item, "getData"):
+ pos = offset_pos
+ elif hasattr(item, "getCountData"):
+ offset_pos += item.size
+ continue
+ else:
+ offset_pos = offset_pos + len(item)
+ continue
+
+ if id(item) not in done:
+ 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
+
+ tables.append(self)
+ obj_list.append((real_links,virtual_links))
+ item_idx += 1
+ done[id(self)] = item_idx
+ if sortCoverageLast:
+ virtual_edges.pop()
+
+ return item_idx
+
+ 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:
+ https://github.com/harfbuzz/harfbuzz/blob/main/docs/repacker.md
+ The input format for hb.repack() method is explained here:
+ 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 = {}
+ objidx = 0
+ virtual_edges = []
+ self._gatherGraphForHarfbuzz(tables, obj_list, done, objidx, virtual_edges)
+ # Gather all data in two passes: the absolute positions of all
+ # subtable are needed before the actual data can be assembled.
+ pos = 0
+ for table in tables:
+ table.pos = pos
+ pos = pos + table.getDataLength()
+
+ data = []
+ for table in tables:
+ tableData = table.getDataForHarfbuzz()
+ data.append(tableData)
+
+ 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."""
+ if remove_duplicate:
+ internedTables = {}
+ self._doneWriting(internedTables)
tables = []
extTables = []
done = {}
@@ -655,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)
@@ -889,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):
@@ -923,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):
@@ -944,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 3929e2f3..327d113f 100644
--- a/Lib/fontTools/ttLib/ttFont.py
+++ b/Lib/fontTools/ttLib/ttFont.py
@@ -1,7 +1,14 @@
+from fontTools.config import Config
from fontTools.misc import xmlWriter
+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
@@ -49,7 +56,7 @@ class TTFont(object):
>> tt2.importXML("afont.ttx")
>> tt2['maxp'].numGlyphs
242
-
+
The TTFont object may be used as a context manager; this will cause the file
reader to be closed after the context ``with`` block is exited::
@@ -89,7 +96,7 @@ class TTFont(object):
sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0,
verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False,
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
- _tableCache=None):
+ _tableCache=None, cfg={}):
for name in ("verbose", "quiet"):
val = locals().get(name)
if val is not None:
@@ -101,6 +108,7 @@ class TTFont(object):
self.recalcTimestamp = recalcTimestamp
self.tables = {}
self.reader = None
+ self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
self.ignoreDecompileErrors = ignoreDecompileErrors
if not file:
@@ -378,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):
@@ -670,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
@@ -681,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")
@@ -698,129 +720,31 @@ class TTFont(object):
return glyphs
def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))):
- """Return the 'best' unicode cmap dictionary available in the font,
- or None, if no unicode cmap subtable is available.
+ """Returns the 'best' Unicode cmap dictionary available in the font
+ or ``None``, if no Unicode cmap subtable is available.
By default it will search for the following (platformID, platEncID)
- pairs::
-
- (3, 10),
- (0, 6),
- (0, 4),
- (3, 1),
- (0, 3),
- (0, 2),
- (0, 1),
- (0, 0)
-
- This can be customized via the ``cmapPreferences`` argument.
+ pairs in order::
+
+ (3, 10), # Windows Unicode full repertoire
+ (0, 6), # Unicode full repertoire (format 13 subtable)
+ (0, 4), # Unicode 2.0 full repertoire
+ (3, 1), # Windows Unicode BMP
+ (0, 3), # Unicode 2.0 BMP
+ (0, 2), # Unicode ISO/IEC 10646
+ (0, 1), # Unicode 1.1
+ (0, 0) # Unicode 1.0
+
+ This particular order matches what HarfBuzz uses to choose what
+ subtable to use by default. This order prefers the largest-repertoire
+ subtable, and among those, prefers the Windows-platform over the
+ Unicode-platform as the former has wider support.
+
+ This order can be customized via the ``cmapPreferences`` argument.
"""
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 44622a14..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
@@ -95,11 +98,11 @@ class Glyph:
self.glyphName = glyphName
self.glyphSet = glyphSet
- def draw(self, pen):
+ def draw(self, pen, outputImpliedClosingLine=False):
"""
Draw this glyph onto a *FontTools* Pen.
"""
- pointPen = PointToSegmentPen(pen)
+ pointPen = PointToSegmentPen(pen, outputImpliedClosingLine=outputImpliedClosingLine)
self.drawPoints(pointPen)
def drawPoints(self, pointPen):
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 15c2e700..f1ca99ff 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -18,6 +18,7 @@ Then you can make a variable-font this way:
API *will* change in near future.
"""
+from typing import List
from fontTools.misc.vector import Vector
from fontTools.misc.roundTools import noRound, otRound
from fontTools.misc.textTools import Tag, tostr
@@ -29,11 +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
+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
@@ -53,7 +58,7 @@ FEAVAR_FEATURETAG_LIB_KEY = "com.github.fonttools.varLib.featureVarsFeatureTag"
# Creation routines
#
-def _add_fvar(font, axes, instances):
+def _add_fvar(font, axes, instances: List[InstanceDescriptor]):
"""
Add 'fvar' table to font.
@@ -81,7 +86,8 @@ def _add_fvar(font, axes, instances):
fvar.axes.append(axis)
for instance in instances:
- coordinates = instance.location
+ # Filter out discrete axis locations
+ coordinates = {name: value for name, value in instance.location.items() if name in axes}
if "en" not in instance.localisedStyleName:
if not instance.styleName:
@@ -198,11 +204,10 @@ def _add_avar(font, axes):
return avar
-def _add_stat(font, axes):
- # for now we just get the axis tags and nameIDs from the fvar,
- # so we can reuse the same nameIDs which were defined in there.
- # TODO make use of 'axes' once it adds style attributes info:
- # https://github.com/LettError/designSpaceDocument/issues/8
+def _add_stat(font):
+ # Note: this function only gets called by old code that calls `build()`
+ # directly. Newer code that wants to benefit from STAT data from the
+ # designspace should call `build_many()`
if "STAT" in font:
return
@@ -483,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)
@@ -603,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
@@ -618,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
@@ -708,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
@@ -759,7 +777,8 @@ def load_designspace(designspace):
# Check all master and instance locations are valid and fill in defaults
for obj in masters+instances:
obj_name = obj.name or obj.styleName or ''
- loc = obj.location
+ loc = obj.getFullDesignLocation(ds)
+ obj.designLocation = loc
if loc is None:
raise VarLibValidationError(
f"Source or instance '{obj_name}' has no location."
@@ -770,22 +789,18 @@ def load_designspace(designspace):
f"Location axis '{axis_name}' unknown for '{obj_name}'."
)
for axis_name,axis in axes.items():
- if axis_name not in loc:
- # NOTE: `axis.default` is always user-space, but `obj.location` always design-space.
- loc[axis_name] = axis.map_forward(axis.default)
- else:
- v = axis.map_backward(loc[axis_name])
- if not (axis.minimum <= v <= axis.maximum):
- raise VarLibValidationError(
- f"Source or instance '{obj_name}' has out-of-range location "
- f"for axis '{axis_name}': is mapped to {v} but must be in "
- f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
- "values are in user-space)."
- )
+ v = axis.map_backward(loc[axis_name])
+ if not (axis.minimum <= v <= axis.maximum):
+ raise VarLibValidationError(
+ f"Source or instance '{obj_name}' has out-of-range location "
+ f"for axis '{axis_name}': is mapped to {v} but must be in "
+ f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
+ "values are in user-space)."
+ )
# Normalize master locations
- internal_master_locs = [o.location for o in masters]
+ internal_master_locs = [o.getFullDesignLocation(ds) for o in masters]
log.info("Internal master locations:\n%s", pformat(internal_master_locs))
# TODO This mapping should ideally be moved closer to logic in _add_fvar/avar
@@ -865,7 +880,53 @@ def set_default_weight_width_slant(font, location):
font["post"].italicAngle = italicAngle
-def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
+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.
+
+ If master_finder is set, it should be a callable that takes master
+ filename as found in designspace file and map it to master font
+ binary as to be opened (eg. .ttf or .otf).
+
+ skip_vf can be used to skip building some of the variable fonts defined in
+ the input designspace. It's a predicate that takes as argument the name
+ of the variable font and returns `bool`.
+
+ Always returns a Dict[str, TTFont] keyed by VariableFontDescriptor.name
+ """
+ res = {}
+ for _location, subDoc in splitInterpolable(designspace):
+ for name, vfDoc in splitVariableFonts(subDoc):
+ if skip_vf(name):
+ log.debug(f"Skipping variable TTF font: {name}")
+ continue
+ vf = build(
+ vfDoc,
+ master_finder,
+ exclude=list(exclude) + ["STAT"],
+ 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,
+ colr_layer_reuse=True,
+):
"""
Build variation font from a designspace file.
@@ -898,7 +959,7 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
# TODO append masters as named-instances as well; needs .designspace change.
fvar = _add_fvar(vf, ds.axes, ds.instances)
if 'STAT' not in exclude:
- _add_stat(vf, ds.axes)
+ _add_stat(vf)
if 'avar' not in exclude:
_add_avar(vf, ds.axes)
@@ -943,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}
@@ -1051,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=(
@@ -1088,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 cec802f3..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
@@ -1024,8 +1008,11 @@ def instantiateSTAT(varfont, axisLimits):
log.info("Instantiating STAT table")
newAxisValueTables = axisValuesFromAxisLimits(stat, axisLimits)
- stat.AxisValueArray.AxisValue = newAxisValueTables
- stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
+ stat.AxisValueCount = len(newAxisValueTables)
+ if stat.AxisValueCount:
+ stat.AxisValueArray.AxisValue = newAxisValueTables
+ else:
+ stat.AxisValueArray = None
def axisValuesFromAxisLimits(stat, axisLimits):
@@ -1196,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))
@@ -1269,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():
@@ -1377,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."
@@ -1414,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)
@@ -1434,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 cff76ece..f86b6f9b 100644
--- a/Lib/fontTools/varLib/interpolatable.py
+++ b/Lib/fontTools/varLib/interpolatable.py
@@ -7,6 +7,7 @@ $ fonttools varLib.interpolatable font1 font2 ...
"""
from fontTools.pens.basePen import AbstractPen, BasePen
+from fontTools.pens.pointPen import SegmentToPointPen
from fontTools.pens.recordingPen import RecordingPen
from fontTools.pens.statisticsPen import StatisticsPen
from fontTools.pens.momentsPen import OpenContourError
@@ -14,6 +15,14 @@ from collections import OrderedDict
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]
+
class PerContourPen(BasePen):
def __init__(self, Pen, glyphset=None):
@@ -55,6 +64,21 @@ class PerContourOrComponentPen(PerContourPen):
self.value[-1].addComponent(glyphName, transformation)
+class RecordingPointPen(BasePen):
+
+ def __init__(self):
+ self.value = []
+
+ def beginPath(self, identifier = None, **kwargs):
+ pass
+
+ def endPath(self) -> None:
+ pass
+
+ def addPoint(self, pt, segmentType=None):
+ self.value.append((pt, False if segmentType is None else True))
+
+
def _vdiff(v0, v1):
return tuple(b - a for a, b in zip(v0, v1))
@@ -65,6 +89,12 @@ def _vlen(vec):
v += x * x
return v
+def _complex_vlen(vec):
+ v = 0
+ for x in vec:
+ v += abs(x) * abs(x)
+ return v
+
def _matching_cost(G, matching):
return sum(G[i][j] for i, j in enumerate(matching))
@@ -125,6 +155,7 @@ def test(glyphsets, glyphs=None, names=None):
try:
allVectors = []
allNodeTypes = []
+ allContourIsomorphisms = []
for glyphset, name in zip(glyphsets, names):
# print('.', end='')
if glyph_name not in glyphset:
@@ -135,18 +166,24 @@ def test(glyphsets, glyphs=None, names=None):
perContourPen = PerContourOrComponentPen(
RecordingPen, glyphset=glyphset
)
- glyph.draw(perContourPen)
+ try:
+ glyph.draw(perContourPen, outputImpliedClosingLine=True)
+ except TypeError:
+ glyph.draw(perContourPen)
contourPens = perContourPen.value
del perContourPen
contourVectors = []
+ contourIsomorphisms = []
nodeTypes = []
allNodeTypes.append(nodeTypes)
allVectors.append(contourVectors)
+ allContourIsomorphisms.append(contourIsomorphisms)
for ix, contour in enumerate(contourPens):
- nodeTypes.append(
- tuple(instruction[0] for instruction in contour.value)
- )
+
+ nodeVecs = tuple(instruction[0] for instruction in contour.value)
+ nodeTypes.append(nodeVecs)
+
stats = StatisticsPen(glyphset=glyphset)
try:
contour.replay(stats)
@@ -168,6 +205,38 @@ def test(glyphsets, glyphs=None, names=None):
contourVectors.append(vector)
# print(vector)
+ # Check starting point
+ if nodeVecs[0] == 'addComponent':
+ continue
+ assert nodeVecs[0] == 'moveTo'
+ assert nodeVecs[-1] in ('closePath', 'endPath')
+ points = RecordingPointPen()
+ converter = SegmentToPointPen(points, False)
+ contour.replay(converter)
+ # points.value is a list of pt,bool where bool is true if on-curve and false if off-curve;
+ # now check all rotations and mirror-rotations of the contour and build list of isomorphic
+ # possible starting points.
+ bits = 0
+ for pt,b in points.value:
+ bits = (bits << 1) | b
+ n = len(points.value)
+ mask = (1 << n ) - 1
+ isomorphisms = []
+ contourIsomorphisms.append(isomorphisms)
+ for i in range(n):
+ b = ((bits << i) & mask) | ((bits >> (n - i)))
+ if b == bits:
+ isomorphisms.append(_rot_list ([complex(*pt) for pt,bl in points.value], i))
+ # Add mirrored rotations
+ mirrored = list(reversed(points.value))
+ reversed_bits = 0
+ for pt,b in mirrored:
+ reversed_bits = (reversed_bits << 1) | b
+ for i in range(n):
+ b = ((reversed_bits << i) & mask) | ((reversed_bits >> (n - i)))
+ if b == bits:
+ isomorphisms.append(_rot_list ([complex(*pt) for pt,bl in mirrored], i))
+
# Check each master against the next one in the list.
for i, (m0, m1) in enumerate(zip(allNodeTypes[:-1], allNodeTypes[1:])):
if len(m0) != len(m1):
@@ -223,7 +292,9 @@ def test(glyphsets, glyphs=None, names=None):
continue
costs = [[_vlen(_vdiff(v0, v1)) for v1 in m1] for v0 in m0]
matching, matching_cost = min_cost_perfect_bipartite_matching(costs)
- if matching != list(range(len(m0))):
+ identity_matching = list(range(len(m0)))
+ identity_cost = sum(costs[i][i] for i in range(len(m0)))
+ if matching != identity_matching and matching_cost < identity_cost * .95:
add_problem(
glyph_name,
{
@@ -235,23 +306,27 @@ def test(glyphsets, glyphs=None, names=None):
},
)
break
- upem = 2048
- item_cost = round(
- (matching_cost / len(m0) / len(m0[0])) ** 0.5 / upem * 100
- )
- hist.append(item_cost)
- threshold = 7
- if item_cost >= threshold:
- add_problem(
- glyph_name,
- {
- "type": "high_cost",
- "master_1": names[i],
- "master_2": names[i + 1],
- "value_1": item_cost,
- "value_2": threshold,
- },
- )
+
+ for i, (m0, m1) in enumerate(zip(allContourIsomorphisms[:-1], allContourIsomorphisms[1:])):
+ if len(m0) != len(m1):
+ # We already reported this
+ continue
+ if not m0:
+ continue
+ for contour0,contour1 in zip(m0,m1):
+ c0 = contour0[0]
+ costs = [v for v in (_complex_vlen(_vdiff(c0, c1)) for c1 in contour1)]
+ min_cost = min(costs)
+ first_cost = costs[0]
+ if min_cost < first_cost * .95:
+ add_problem(
+ glyph_name,
+ {
+ "type": "wrong_start_point",
+ "master_1": names[i],
+ "master_2": names[i + 1],
+ },
+ )
except ValueError as e:
add_problem(
@@ -286,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
@@ -351,14 +475,12 @@ def main(args=None):
p["master_2"],
)
)
- if p["type"] == "high_cost":
+ if p["type"] == "wrong_start_point":
print(
- " Interpolation has high cost: cost of %s to %s = %i, threshold %i"
+ " Contour start point differs: %s, %s"
% (
p["master_1"],
p["master_2"],
- p["value_1"],
- p["value_2"],
)
)
if problems:
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 5a3a4f34..c9a1d3e3 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -3,22 +3,26 @@ 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
from fontTools.otlLib.optimize.gpos import (
- compact_pair_pos,
- GPOS_COMPACT_MODE_DEFAULT,
- GPOS_COMPACT_MODE_ENV_KEY,
+ _compression_level_from_env,
+ compact_pair_pos,
)
log = logging.getLogger("fontTools.varLib.merger")
@@ -27,11 +31,12 @@ from .errors import (
ShouldBeConstant,
FoundANone,
MismatchedTypes,
+ NotANone,
LengthsDiffer,
KeysDiffer,
InconsistentGlyphOrder,
InconsistentExtensions,
- UnsupportedFormat,
+ InconsistentFormats,
UnsupportedFormat,
VarLibMergeError,
)
@@ -40,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,)
@@ -82,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,
@@ -123,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):
@@ -135,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
@@ -217,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 \
@@ -850,10 +875,14 @@ def merge(merger, self, lst):
# Compact the merged subtables
# This is a good moment to do it because the compaction should create
# smaller subtables, which may prevent overflows from happening.
- mode = os.environ.get(GPOS_COMPACT_MODE_ENV_KEY, GPOS_COMPACT_MODE_DEFAULT)
- if mode and mode != "0":
+ # Keep reading the value from the ENV until ufo2ft switches to the config system
+ level = merger.font.cfg.get(
+ "fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL",
+ default=_compression_level_from_env(),
+ )
+ if level != 0:
log.info("Compacting GPOS...")
- self.SubTable = compact_pair_pos(merger.font, mode, self.SubTable)
+ self.SubTable = compact_pair_pos(merger.font, level, self.SubTable)
self.SubTableCount = len(self.SubTable)
elif isSinglePos and flattened:
@@ -1033,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)
@@ -1046,6 +1083,8 @@ class VariationMerger(AligningMerger):
if masterModel:
self.setModel(masterModel)
+ if origTTFs:
+ self.ttfs = origTTFs
def buildVarDevTable(store_builder, master_values):
@@ -1096,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/stat.py b/Lib/fontTools/varLib/stat.py
new file mode 100644
index 00000000..46c9498d
--- /dev/null
+++ b/Lib/fontTools/varLib/stat.py
@@ -0,0 +1,142 @@
+"""Extra methods for DesignSpaceDocument to generate its STAT table data."""
+
+from __future__ import annotations
+
+from typing import Dict, List, Union
+
+import fontTools.otlLib.builder
+from fontTools.designspaceLib import (
+ AxisLabelDescriptor,
+ DesignSpaceDocument,
+ DesignSpaceDocumentError,
+ LocationLabelDescriptor,
+)
+from fontTools.designspaceLib.types import Region, getVFUserRegion, locationInRegion
+from fontTools.ttLib import TTFont
+
+
+def buildVFStatTable(ttFont: TTFont, doc: DesignSpaceDocument, vfName: str) -> None:
+ """Build the STAT table for the variable font identified by its name in
+ the given document.
+
+ Knowing which variable we're building STAT data for is needed to subset
+ the STAT locations to only include what the variable font actually ships.
+
+ .. versionadded:: 5.0
+
+ .. seealso::
+ - :func:`getStatAxes()`
+ - :func:`getStatLocations()`
+ - :func:`fontTools.otlLib.builder.buildStatTable()`
+ """
+ for vf in doc.getVariableFonts():
+ if vf.name == vfName:
+ break
+ else:
+ raise DesignSpaceDocumentError(
+ f"Cannot find the variable font by name {vfName}"
+ )
+
+ region = getVFUserRegion(doc, vf)
+
+ return fontTools.otlLib.builder.buildStatTable(
+ ttFont,
+ getStatAxes(doc, region),
+ getStatLocations(doc, region),
+ doc.elidedFallbackName if doc.elidedFallbackName is not None else 2,
+ )
+
+
+def getStatAxes(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]:
+ """Return a list of axis dicts suitable for use as the ``axes``
+ argument to :func:`fontTools.otlLib.builder.buildStatTable()`.
+
+ .. versionadded:: 5.0
+ """
+ # First, get the axis labels with explicit ordering
+ # then append the others in the order they appear.
+ maxOrdering = max(
+ (axis.axisOrdering for axis in doc.axes if axis.axisOrdering is not None),
+ default=-1,
+ )
+ axisOrderings = []
+ for axis in doc.axes:
+ if axis.axisOrdering is not None:
+ axisOrderings.append(axis.axisOrdering)
+ else:
+ maxOrdering += 1
+ axisOrderings.append(maxOrdering)
+ return [
+ dict(
+ tag=axis.tag,
+ name={"en": axis.name, **axis.labelNames},
+ ordering=ordering,
+ values=[
+ _axisLabelToStatLocation(label)
+ for label in axis.axisLabels
+ if locationInRegion({axis.name: label.userValue}, userRegion)
+ ],
+ )
+ for axis, ordering in zip(doc.axes, axisOrderings)
+ ]
+
+
+def getStatLocations(doc: DesignSpaceDocument, userRegion: Region) -> List[Dict]:
+ """Return a list of location dicts suitable for use as the ``locations``
+ argument to :func:`fontTools.otlLib.builder.buildStatTable()`.
+
+ .. versionadded:: 5.0
+ """
+ axesByName = {axis.name: axis for axis in doc.axes}
+ return [
+ dict(
+ name={"en": label.name, **label.labelNames},
+ # Location in the designspace is keyed by axis name
+ # Location in buildStatTable by axis tag
+ location={
+ axesByName[name].tag: value
+ for name, value in label.getFullUserLocation(doc).items()
+ },
+ flags=_labelToFlags(label),
+ )
+ for label in doc.locationLabels
+ if locationInRegion(label.getFullUserLocation(doc), userRegion)
+ ]
+
+
+def _labelToFlags(label: Union[AxisLabelDescriptor, LocationLabelDescriptor]) -> int:
+ flags = 0
+ if label.olderSibling:
+ flags |= 1
+ if label.elidable:
+ flags |= 2
+ return flags
+
+
+def _axisLabelToStatLocation(
+ label: AxisLabelDescriptor,
+) -> Dict:
+ label_format = label.getFormat()
+ name = {"en": label.name, **label.labelNames}
+ flags = _labelToFlags(label)
+ if label_format == 1:
+ return dict(name=name, value=label.userValue, flags=flags)
+ if label_format == 3:
+ return dict(
+ name=name,
+ value=label.userValue,
+ linkedValue=label.linkedUserValue,
+ flags=flags,
+ )
+ if label_format == 2:
+ res = dict(
+ name=name,
+ nominalValue=label.userValue,
+ flags=flags,
+ )
+ if label.userMinimum is not None:
+ res["rangeMinValue"] = label.userMinimum
+ if label.userMaximum is not None:
+ res["rangeMaxValue"] = label.userMaximum
+ return res
+ raise NotImplementedError("Unknown STAT label format")
diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py
index bcf81b39..2ffc6b13 100644
--- a/Lib/fontTools/varLib/varStore.py
+++ b/Lib/fontTools/varLib/varStore.py
@@ -7,6 +7,10 @@ from functools import partial
from collections import defaultdict
+NO_VARIATION_INDEX = ot.NO_VARIATION_INDEX
+ot.VarStore.NO_VARIATION_INDEX = NO_VARIATION_INDEX
+
+
def _getLocationKey(loc):
return tuple(sorted(loc.items(), key=lambda kv: kv[0]))
@@ -135,6 +139,11 @@ def VarRegion_get_support(self, fvar_axes):
ot.VarRegion.get_support = VarRegion_get_support
+def VarStore___bool__(self):
+ return bool(self.VarData)
+
+ot.VarStore.__bool__ = VarStore___bool__
+
class VarStoreInstancer(object):
def __init__(self, varstore, fvar_axes, location={}):
@@ -169,6 +178,7 @@ class VarStoreInstancer(object):
def __getitem__(self, varidx):
major, minor = varidx >> 16, varidx & 0xFFFF
+ if varidx == NO_VARIATION_INDEX: return 0.
varData = self._varData
scalars = [self._getScalar(ri) for ri in varData[major].VarRegionIndex]
deltas = varData[major].Item[minor]
@@ -192,6 +202,8 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True, retainFirstMap=False
# Sort out used varIdxes by major/minor.
used = {}
for varIdx in varIdxes:
+ if varIdx == NO_VARIATION_INDEX:
+ continue
major = varIdx >> 16
minor = varIdx & 0xFFFF
d = used.get(major)
@@ -206,7 +218,7 @@ def VarStore_subset_varidxes(self, varIdxes, optimize=True, retainFirstMap=False
varData = self.VarData
newVarData = []
- varDataMap = {}
+ varDataMap = {NO_VARIATION_INDEX: NO_VARIATION_INDEX}
for major,data in enumerate(varData):
usedMinors = used.get(major)
if usedMinors is None:
@@ -431,7 +443,7 @@ class _EncodingDict(dict):
return chars
-def VarStore_optimize(self):
+def VarStore_optimize(self, use_NO_VARIATION_INDEX=True):
"""Optimize storage. Returns mapping from old VarIdxes to new ones."""
# TODO
@@ -455,6 +467,10 @@ def VarStore_optimize(self):
row[regionIdx] += v
row = tuple(row)
+ if use_NO_VARIATION_INDEX and not any(row):
+ front_mapping[(major<<16)+minor] = None
+ continue
+
encodings.add_row(row)
front_mapping[(major<<16)+minor] = row
@@ -537,9 +553,9 @@ def VarStore_optimize(self):
back_mapping[item] = (major<<16)+minor
# Compile final mapping.
- varidx_map = {}
+ varidx_map = {NO_VARIATION_INDEX:NO_VARIATION_INDEX}
for k,v in front_mapping.items():
- varidx_map[k] = back_mapping[v]
+ varidx_map[k] = back_mapping[v] if v is not None else NO_VARIATION_INDEX
# Remove unused regions.
self.prune_regions()
diff --git a/METADATA b/METADATA
index 35f87b65..7ab3b753 100644
--- a/METADATA
+++ b/METADATA
@@ -10,14 +10,14 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/archive/4.31.2.zip"
+ value: "https://github.com/fonttools/fonttools/archive/4.37.1.zip"
}
- version: "4.31.2"
+ version: "4.37.1"
license_type: BY_EXCEPTION_ONLY
license_note: "contains OFL fonts"
last_upgrade_date {
year: 2022
- month: 3
- day: 25
+ month: 8
+ day: 24
}
}
diff --git a/NEWS.rst b/NEWS.rst
index 5971d3cc..f022ad23 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,229 @@
+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)
+----------------------------
+
+- [designspaceLib] Fixed typo in ``deepcopyExceptFonts`` method, preventing font
+ references to be transferred (#2600). Fixed another typo in the name of ``Range``
+ dataclass's ``__post_init__`` magic method (#2597).
+
+4.33.2 (released 2022-04-22)
+----------------------------
+
+- [otBase] Make logging less verbose when harfbuzz fails to serialize. Do not exit
+ at the first failure but continue attempting to fix offset overflow error using
+ the pure-python serializer even when the ``USE_HARFBUZZ_REPACKER`` option was
+ explicitly set to ``True``. This is normal with fonts with relatively large
+ tables, at least until hb.repack implements proper table splitting.
+
+4.33.1 (released 2022-04-22)
+----------------------------
+
+- [otlLib] Put back the ``FONTTOOLS_GPOS_COMPACT_MODE`` environment variable to fix
+ regression in ufo2ft (and thus fontmake) introduced with v4.33.0 (#2592, #2593).
+ This is deprecated and will be removed one ufo2ft gets updated to use the new
+ config setup.
+
+4.33.0 (released 2022-04-21)
+----------------------------
+
+- [OS/2 / merge] Automatically recalculate ``OS/2.xAvgCharWidth`` after merging
+ fonts with ``fontTools.merge`` (#2591, #2538).
+- [misc/config] Added ``fontTools.misc.configTools`` module, a generic configuration
+ system (#2416, #2439).
+ Added ``fontTools.config`` module, a fontTools-specific configuration
+ system using ``configTools`` above.
+ Attached a ``Config`` object to ``TTFont``.
+- [otlLib] Replaced environment variable for GPOS compression level with an
+ equivalent option using the new config system.
+- [designspaceLib] Incremented format version to 5.0 (#2436).
+ Added discrete axes, variable fonts, STAT information, either design- or
+ user-space location on instances.
+ Added ``fontTools.designspaceLib.split`` module to split a designspace
+ into sub-spaces that interpolate and that represent the variable fonts
+ listed in the document.
+ Made instance names optional and allow computing them from STAT data instead.
+ Added ``fontTools.designspaceLib.statNames`` module.
+ Allow instances to have the same location as a previously defined STAT label.
+ Deprecated some attributes:
+ ``SourceDescriptor``: ``copyLib``, ``copyInfo``, ``copyGroups``, ``copyFeatures``.
+ ``InstanceDescriptor``: ``kerning``, ``info``; ``glyphs``: use rules or sparse
+ sources.
+ For both, ``location``: use the more explicit designLocation.
+ Note: all are soft deprecations and existing code should keep working.
+ Updated documentation for Python methods and the XML format.
+- [varLib] Added ``build_many`` to build several variable fonts from a single
+ designspace document (#2436).
+ Added ``fontTools.varLib.stat`` module to build STAT tables from a designspace
+ document.
+- [otBase] Try to use the Harfbuzz Repacker for packing GSUB/GPOS tables when
+ ``uharfbuzz`` python bindings are available (#2552). Disable it by setting the
+ "fontTools.ttLib.tables.otBase:USE_HARFBUZZ_REPACKER" config option to ``False``.
+ If the option is set explicitly to ``True`` but ``uharfbuzz`` can't be imported
+ or fails to serialize for any reasons, an error will be raised (ImportError or
+ uharfbuzz errors).
+- [CFF/T2] Ensure that ``pen.closePath()`` gets called for CFF2 charstrings (#2577).
+ Handle implicit CFF2 closePath within ``T2OutlineExtractor`` (#2580).
+
+4.32.0 (released 2022-04-08)
+----------------------------
+
+- [otlLib] Disable GPOS7 optimization to work around bug in Apple CoreText.
+ Always force Chaining GPOS8 for now (#2540).
+- [glifLib] Added ``outputImpliedClosingLine=False`` parameter to ``Glyph.draw()``,
+ to control behaviour of ``PointToSegmentPen`` (6b4e2e7).
+- [varLib.interpolatable] Check for wrong contour starting point (#2571).
+- [cffLib] Remove leftover ``GlobalState`` class and fix calls to ``TopDictIndex()``
+ (#2569, #2570).
+- [instancer] Clear ``AxisValueArray`` if it is empty after instantiating (#2563).
+
4.31.2 (released 2022-03-22)
----------------------------
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/config_test.py b/Tests/config_test.py
new file mode 100644
index 00000000..58163913
--- /dev/null
+++ b/Tests/config_test.py
@@ -0,0 +1,135 @@
+from fontTools.misc.configTools import AbstractConfig, Options
+import pytest
+from fontTools.config import (
+ OPTIONS,
+ Config,
+ ConfigUnknownOptionError,
+ ConfigValueParsingError,
+ ConfigValueValidationError,
+)
+from fontTools.ttLib import TTFont
+
+
+def test_can_register_option():
+ MY_OPTION = Config.register_option(
+ name="tests:MY_OPTION",
+ help="Test option, value should be True or False, default = True",
+ default=True,
+ parse=lambda v: v in ("True", "true", 1, True),
+ validate=lambda v: v == True or v == False,
+ )
+
+ assert MY_OPTION.name == "tests:MY_OPTION"
+ assert (
+ MY_OPTION.help == "Test option, value should be True or False, default = True"
+ )
+ assert MY_OPTION.default == True
+ assert MY_OPTION.parse("true") == True
+ assert MY_OPTION.validate("hello") == False
+
+ ttFont = TTFont(cfg={"tests:MY_OPTION": True})
+ assert True == ttFont.cfg.get("tests:MY_OPTION")
+ assert True == ttFont.cfg.get(MY_OPTION)
+
+
+# to parametrize tests of Config mapping interface accepting either a str or Option
+@pytest.fixture(
+ params=[
+ pytest.param("fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL", id="str"),
+ pytest.param(
+ OPTIONS["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"], id="Option"
+ ),
+ ]
+)
+def COMPRESSION_LEVEL(request):
+ return request.param
+
+
+def test_ttfont_has_config(COMPRESSION_LEVEL):
+ ttFont = TTFont(cfg={COMPRESSION_LEVEL: 8})
+ assert 8 == ttFont.cfg.get(COMPRESSION_LEVEL)
+
+
+def test_ttfont_can_take_superset_of_fonttools_config(COMPRESSION_LEVEL):
+ # Create MyConfig with all options from fontTools.config plus some
+ my_options = Options(Config.options)
+ MY_OPTION = my_options.register("custom:my_option", "help", "default", str, any)
+
+ class MyConfig(AbstractConfig):
+ options = my_options
+
+ ttFont = TTFont(cfg=MyConfig({"custom:my_option": "my_value"}))
+ assert 0 == ttFont.cfg.get(COMPRESSION_LEVEL)
+ assert "my_value" == ttFont.cfg.get(MY_OPTION)
+
+ # but the default Config doens't know about MY_OPTION
+ with pytest.raises(ConfigUnknownOptionError):
+ TTFont(cfg={MY_OPTION: "my_value"})
+
+
+def test_no_config_returns_default_values(COMPRESSION_LEVEL):
+ ttFont = TTFont()
+ assert 0 == ttFont.cfg.get(COMPRESSION_LEVEL)
+ assert 3 == ttFont.cfg.get(COMPRESSION_LEVEL, 3)
+
+
+def test_can_set_config(COMPRESSION_LEVEL):
+ ttFont = TTFont()
+ ttFont.cfg.set(COMPRESSION_LEVEL, 5)
+ assert 5 == ttFont.cfg.get(COMPRESSION_LEVEL)
+ ttFont.cfg.set(COMPRESSION_LEVEL, 6)
+ assert 6 == ttFont.cfg.get(COMPRESSION_LEVEL)
+
+
+def test_different_ttfonts_have_different_configs(COMPRESSION_LEVEL):
+ cfg = Config({COMPRESSION_LEVEL: 5})
+ ttFont1 = TTFont(cfg=cfg)
+ ttFont2 = TTFont(cfg=cfg)
+ ttFont2.cfg.set(COMPRESSION_LEVEL, 6)
+ assert 5 == ttFont1.cfg.get(COMPRESSION_LEVEL)
+ assert 6 == ttFont2.cfg.get(COMPRESSION_LEVEL)
+
+
+def test_cannot_set_inexistent_key():
+ with pytest.raises(ConfigUnknownOptionError):
+ TTFont(cfg={"notALib.notAModule.inexistent": 4})
+
+
+def test_value_not_parsed_by_default(COMPRESSION_LEVEL):
+ # Note: value given as a string
+ with pytest.raises(ConfigValueValidationError):
+ TTFont(cfg={COMPRESSION_LEVEL: "8"})
+
+
+def test_value_gets_parsed_if_asked(COMPRESSION_LEVEL):
+ # Note: value given as a string
+ ttFont = TTFont(cfg=Config({COMPRESSION_LEVEL: "8"}, parse_values=True))
+ assert 8 == ttFont.cfg.get(COMPRESSION_LEVEL)
+
+
+def test_value_parsing_can_error(COMPRESSION_LEVEL):
+ with pytest.raises(ConfigValueParsingError):
+ TTFont(
+ cfg=Config(
+ {COMPRESSION_LEVEL: "not an int"},
+ parse_values=True,
+ )
+ )
+
+
+def test_value_gets_validated(COMPRESSION_LEVEL):
+ # Note: 12 is not a valid value for GPOS compression level (must be in 0-9)
+ with pytest.raises(ConfigValueValidationError):
+ TTFont(cfg={COMPRESSION_LEVEL: 12})
+
+
+def test_implements_mutable_mapping(COMPRESSION_LEVEL):
+ cfg = Config()
+ cfg[COMPRESSION_LEVEL] = 2
+ assert 2 == cfg[COMPRESSION_LEVEL]
+ assert list(iter(cfg))
+ assert 1 == len(cfg)
+ del cfg[COMPRESSION_LEVEL]
+ assert 0 == cfg[COMPRESSION_LEVEL]
+ assert not list(iter(cfg))
+ assert 0 == len(cfg)
diff --git a/Tests/designspaceLib/__init__.py b/Tests/designspaceLib/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/Tests/designspaceLib/__init__.py
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/convert5to4_output/AktivGroteskVF_Italics_Wght.designspace b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_Wght.designspace
new file mode 100644
index 00000000..aa8e4f9c
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_Wght.designspace
@@ -0,0 +1,114 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_WghtWdth.designspace b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_WghtWdth.designspace
new file mode 100644
index 00000000..2e22c019
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Italics_WghtWdth.designspace
@@ -0,0 +1,316 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_HairIt.ufo" name="Aktiv Grotesk Cd Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_HairIt.ufo" name="Aktiv Grotesk Ex Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_It.ufo" name="Aktiv Grotesk Cd Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_It.ufo" name="Aktiv Grotesk Ex Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_BlkIt.ufo" name="Aktiv Grotesk Cd Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_BlkIt.ufo" name="Aktiv Grotesk Ex Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair Italic" familyname="Aktiv Grotesk" stylename="Cd Hair Italic" filename="../instances/AktivGroteskCd_HairIt.ufo" postscriptfontname="AktivGrotesk-CdHairItalic" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair Italic" familyname="Aktiv Grotesk" stylename="Ex Hair Italic" filename="../instances/AktivGroteskEx_HairIt.ufo" postscriptfontname="AktivGrotesk-ExHairItalic" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin Italic" familyname="Aktiv Grotesk" stylename="Cd Thin Italic" filename="../instances/AktivGroteskCd_ThIt.ufo" postscriptfontname="AktivGrotesk-CdThinItalic" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin Italic" familyname="Aktiv Grotesk" stylename="Ex Thin Italic" filename="../instances/AktivGroteskEx_ThIt.ufo" postscriptfontname="AktivGrotesk-ExThinItalic" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light Italic" familyname="Aktiv Grotesk" stylename="Cd Light Italic" filename="../instances/AktivGroteskCd_LtIt.ufo" postscriptfontname="AktivGrotesk-CdLightItalic" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light Italic" familyname="Aktiv Grotesk" stylename="Ex Light Italic" filename="../instances/AktivGroteskEx_LtIt.ufo" postscriptfontname="AktivGrotesk-ExLightItalic" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Italic" familyname="Aktiv Grotesk" stylename="Cd Italic" filename="../instances/AktivGroteskCd_It.ufo" postscriptfontname="AktivGrotesk-CdItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Italic" familyname="Aktiv Grotesk" stylename="Ex Italic" filename="../instances/AktivGroteskEx_It.ufo" postscriptfontname="AktivGrotesk-ExItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium Italic" familyname="Aktiv Grotesk" stylename="Cd Medium Italic" filename="../instances/AktivGroteskCd_MdIt.ufo" postscriptfontname="AktivGrotesk-CdMediumItalic" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium Italic" familyname="Aktiv Grotesk" stylename="Ex Medium Italic" filename="../instances/AktivGroteskEx_MdIt.ufo" postscriptfontname="AktivGrotesk-ExMediumItalic" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold Italic" familyname="Aktiv Grotesk" stylename="Cd SemiBold Italic" filename="../../build/instances/AktivGroteskCd_SBdIt.ufo" postscriptfontname="AktivGrotesk-CdSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold Italic" familyname="Aktiv Grotesk" stylename="Ex SemiBold Italic" filename="../../build/instances/AktivGroteskEx_SBdIt.ufo" postscriptfontname="AktivGrotesk-ExSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold Italic" familyname="Aktiv Grotesk" stylename="Cd Bold Italic" filename="../instances/AktivGroteskCd_BdIt.ufo" postscriptfontname="AktivGrotesk-CdBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold Italic" familyname="Aktiv Grotesk" stylename="Ex Bold Italic" filename="../instances/AktivGroteskEx_BdIt.ufo" postscriptfontname="AktivGrotesk-ExBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold Italic" familyname="Aktiv Grotesk" stylename="Cd XBold Italic" filename="../instances/AktivGroteskCd_XBdIt.ufo" postscriptfontname="AktivGrotesk-CdXBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold Italic" familyname="Aktiv Grotesk" stylename="Ex XBold Italic" filename="../instances/AktivGroteskEx_XBdIt.ufo" postscriptfontname="AktivGrotesk-ExXBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black Italic" familyname="Aktiv Grotesk" stylename="Cd Black Italic" filename="../instances/AktivGroteskCd_BlkIt.ufo" postscriptfontname="AktivGrotesk-CdBlackItalic" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black Italic" familyname="Aktiv Grotesk" stylename="Ex Black Italic" filename="../instances/AktivGroteskEx_BlkIt.ufo" postscriptfontname="AktivGrotesk-ExBlackItalic" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Wght.designspace b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Wght.designspace
new file mode 100644
index 00000000..2ae35f77
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_Wght.designspace
@@ -0,0 +1,103 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ </axes>
+ <sources>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdth.designspace b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdth.designspace
new file mode 100644
index 00000000..219d2262
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdth.designspace
@@ -0,0 +1,307 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_Hair.ufo" name="Aktiv Grotesk Cd Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Hair.ufo" name="Aktiv Grotesk Ex Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Rg.ufo" name="Aktiv Grotesk Cd">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Rg.ufo" name="Aktiv Grotesk Ex">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Blk.ufo" name="Aktiv Grotesk Cd Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Blk.ufo" name="Aktiv Grotesk Ex Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair" familyname="Aktiv Grotesk" stylename="Cd Hair" filename="../instances/AktivGroteskCd_Hair.ufo" postscriptfontname="AktivGrotesk-CdHair" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair" familyname="Aktiv Grotesk" stylename="Ex Hair" filename="../instances/AktivGroteskEx_Hair.ufo" postscriptfontname="AktivGrotesk-ExHair" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin" familyname="Aktiv Grotesk" stylename="Cd Thin" filename="../instances/AktivGroteskCd_Th.ufo" postscriptfontname="AktivGrotesk-CdThin" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin" familyname="Aktiv Grotesk" stylename="Ex Thin" filename="../instances/AktivGroteskEx_Th.ufo" postscriptfontname="AktivGrotesk-ExThin" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light" familyname="Aktiv Grotesk" stylename="Cd Light" filename="../instances/AktivGroteskCd_Lt.ufo" postscriptfontname="AktivGrotesk-CdLight" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light" familyname="Aktiv Grotesk" stylename="Ex Light" filename="../instances/AktivGroteskEx_Lt.ufo" postscriptfontname="AktivGrotesk-ExLight" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd" familyname="Aktiv Grotesk" stylename="Cd" filename="../instances/AktivGroteskCd_Rg.ufo" postscriptfontname="AktivGrotesk-Cd" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex" familyname="Aktiv Grotesk" stylename="Ex" filename="../instances/AktivGroteskEx_Rg.ufo" postscriptfontname="AktivGrotesk-Ex" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium" familyname="Aktiv Grotesk" stylename="Cd Medium" filename="../instances/AktivGroteskCd_Md.ufo" postscriptfontname="AktivGrotesk-CdMedium" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium" familyname="Aktiv Grotesk" stylename="Ex Medium" filename="../instances/AktivGroteskEx_Md.ufo" postscriptfontname="AktivGrotesk-ExMedium" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold" familyname="Aktiv Grotesk" stylename="Cd SemiBold" filename="../../build/instances/AktivGroteskCd_SBd.ufo" postscriptfontname="AktivGrotesk-CdSemiBold" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold" familyname="Aktiv Grotesk" stylename="Ex SemiBold" filename="../../build/instances/AktivGroteskEx_SBd.ufo" postscriptfontname="AktivGrotesk-ExSemiBold" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold" familyname="Aktiv Grotesk" stylename="Cd Bold" filename="../instances/AktivGroteskCd_Bd.ufo" postscriptfontname="AktivGrotesk-CdBold" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold" familyname="Aktiv Grotesk" stylename="Ex Bold" filename="../instances/AktivGroteskEx_Bd.ufo" postscriptfontname="AktivGrotesk-ExBold" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold" familyname="Aktiv Grotesk" stylename="Cd XBold" filename="../instances/AktivGroteskCd_XBd.ufo" postscriptfontname="AktivGrotesk-CdXBold" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold" familyname="Aktiv Grotesk" stylename="Ex XBold" filename="../instances/AktivGroteskEx_XBd.ufo" postscriptfontname="AktivGrotesk-ExXBold" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black" familyname="Aktiv Grotesk" stylename="Cd Black" filename="../instances/AktivGroteskCd_Blk.ufo" postscriptfontname="AktivGrotesk-CdBlack" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black" familyname="Aktiv Grotesk" stylename="Ex Black" filename="../instances/AktivGroteskEx_Blk.ufo" postscriptfontname="AktivGrotesk-ExBlack" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdthItal.designspace b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdthItal.designspace
new file mode 100644
index 00000000..5b419bad
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/AktivGroteskVF_WghtWdthItal.designspace
@@ -0,0 +1,670 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ <axis tag="ital" name="Italic" minimum="0" maximum="1" default="0"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <conditionset>
+ <condition name="Italic" minimum="0.1" maximum="1"/>
+ </conditionset>
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_Hair.ufo" name="Aktiv Grotesk Cd Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_HairIt.ufo" name="Aktiv Grotesk Cd Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Hair.ufo" name="Aktiv Grotesk Ex Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_HairIt.ufo" name="Aktiv Grotesk Ex Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Rg.ufo" name="Aktiv Grotesk Cd">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_It.ufo" name="Aktiv Grotesk Cd Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Rg.ufo" name="Aktiv Grotesk Ex">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_It.ufo" name="Aktiv Grotesk Ex Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Blk.ufo" name="Aktiv Grotesk Cd Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_BlkIt.ufo" name="Aktiv Grotesk Cd Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Blk.ufo" name="Aktiv Grotesk Ex Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_BlkIt.ufo" name="Aktiv Grotesk Ex Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair" familyname="Aktiv Grotesk" stylename="Cd Hair" filename="../instances/AktivGroteskCd_Hair.ufo" postscriptfontname="AktivGrotesk-CdHair" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Hair Italic" familyname="Aktiv Grotesk" stylename="Cd Hair Italic" filename="../instances/AktivGroteskCd_HairIt.ufo" postscriptfontname="AktivGrotesk-CdHairItalic" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair" familyname="Aktiv Grotesk" stylename="Ex Hair" filename="../instances/AktivGroteskEx_Hair.ufo" postscriptfontname="AktivGrotesk-ExHair" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair Italic" familyname="Aktiv Grotesk" stylename="Ex Hair Italic" filename="../instances/AktivGroteskEx_HairIt.ufo" postscriptfontname="AktivGrotesk-ExHairItalic" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin" familyname="Aktiv Grotesk" stylename="Cd Thin" filename="../instances/AktivGroteskCd_Th.ufo" postscriptfontname="AktivGrotesk-CdThin" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin Italic" familyname="Aktiv Grotesk" stylename="Cd Thin Italic" filename="../instances/AktivGroteskCd_ThIt.ufo" postscriptfontname="AktivGrotesk-CdThinItalic" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin" familyname="Aktiv Grotesk" stylename="Ex Thin" filename="../instances/AktivGroteskEx_Th.ufo" postscriptfontname="AktivGrotesk-ExThin" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin Italic" familyname="Aktiv Grotesk" stylename="Ex Thin Italic" filename="../instances/AktivGroteskEx_ThIt.ufo" postscriptfontname="AktivGrotesk-ExThinItalic" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light" familyname="Aktiv Grotesk" stylename="Cd Light" filename="../instances/AktivGroteskCd_Lt.ufo" postscriptfontname="AktivGrotesk-CdLight" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light Italic" familyname="Aktiv Grotesk" stylename="Cd Light Italic" filename="../instances/AktivGroteskCd_LtIt.ufo" postscriptfontname="AktivGrotesk-CdLightItalic" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light" familyname="Aktiv Grotesk" stylename="Ex Light" filename="../instances/AktivGroteskEx_Lt.ufo" postscriptfontname="AktivGrotesk-ExLight" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light Italic" familyname="Aktiv Grotesk" stylename="Ex Light Italic" filename="../instances/AktivGroteskEx_LtIt.ufo" postscriptfontname="AktivGrotesk-ExLightItalic" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd" familyname="Aktiv Grotesk" stylename="Cd" filename="../instances/AktivGroteskCd_Rg.ufo" postscriptfontname="AktivGrotesk-Cd" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Italic" familyname="Aktiv Grotesk" stylename="Cd Italic" filename="../instances/AktivGroteskCd_It.ufo" postscriptfontname="AktivGrotesk-CdItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex" familyname="Aktiv Grotesk" stylename="Ex" filename="../instances/AktivGroteskEx_Rg.ufo" postscriptfontname="AktivGrotesk-Ex" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Italic" familyname="Aktiv Grotesk" stylename="Ex Italic" filename="../instances/AktivGroteskEx_It.ufo" postscriptfontname="AktivGrotesk-ExItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium" familyname="Aktiv Grotesk" stylename="Cd Medium" filename="../instances/AktivGroteskCd_Md.ufo" postscriptfontname="AktivGrotesk-CdMedium" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium Italic" familyname="Aktiv Grotesk" stylename="Cd Medium Italic" filename="../instances/AktivGroteskCd_MdIt.ufo" postscriptfontname="AktivGrotesk-CdMediumItalic" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium" familyname="Aktiv Grotesk" stylename="Ex Medium" filename="../instances/AktivGroteskEx_Md.ufo" postscriptfontname="AktivGrotesk-ExMedium" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium Italic" familyname="Aktiv Grotesk" stylename="Ex Medium Italic" filename="../instances/AktivGroteskEx_MdIt.ufo" postscriptfontname="AktivGrotesk-ExMediumItalic" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold" familyname="Aktiv Grotesk" stylename="Cd SemiBold" filename="../../build/instances/AktivGroteskCd_SBd.ufo" postscriptfontname="AktivGrotesk-CdSemiBold" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold Italic" familyname="Aktiv Grotesk" stylename="Cd SemiBold Italic" filename="../../build/instances/AktivGroteskCd_SBdIt.ufo" postscriptfontname="AktivGrotesk-CdSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold" familyname="Aktiv Grotesk" stylename="Ex SemiBold" filename="../../build/instances/AktivGroteskEx_SBd.ufo" postscriptfontname="AktivGrotesk-ExSemiBold" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold Italic" familyname="Aktiv Grotesk" stylename="Ex SemiBold Italic" filename="../../build/instances/AktivGroteskEx_SBdIt.ufo" postscriptfontname="AktivGrotesk-ExSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold" familyname="Aktiv Grotesk" stylename="Cd Bold" filename="../instances/AktivGroteskCd_Bd.ufo" postscriptfontname="AktivGrotesk-CdBold" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold Italic" familyname="Aktiv Grotesk" stylename="Cd Bold Italic" filename="../instances/AktivGroteskCd_BdIt.ufo" postscriptfontname="AktivGrotesk-CdBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold" familyname="Aktiv Grotesk" stylename="Ex Bold" filename="../instances/AktivGroteskEx_Bd.ufo" postscriptfontname="AktivGrotesk-ExBold" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold Italic" familyname="Aktiv Grotesk" stylename="Ex Bold Italic" filename="../instances/AktivGroteskEx_BdIt.ufo" postscriptfontname="AktivGrotesk-ExBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold" familyname="Aktiv Grotesk" stylename="Cd XBold" filename="../instances/AktivGroteskCd_XBd.ufo" postscriptfontname="AktivGrotesk-CdXBold" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold Italic" familyname="Aktiv Grotesk" stylename="Cd XBold Italic" filename="../instances/AktivGroteskCd_XBdIt.ufo" postscriptfontname="AktivGrotesk-CdXBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold" familyname="Aktiv Grotesk" stylename="Ex XBold" filename="../instances/AktivGroteskEx_XBd.ufo" postscriptfontname="AktivGrotesk-ExXBold" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold Italic" familyname="Aktiv Grotesk" stylename="Ex XBold Italic" filename="../instances/AktivGroteskEx_XBdIt.ufo" postscriptfontname="AktivGrotesk-ExXBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black" familyname="Aktiv Grotesk" stylename="Cd Black" filename="../instances/AktivGroteskCd_Blk.ufo" postscriptfontname="AktivGrotesk-CdBlack" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black Italic" familyname="Aktiv Grotesk" stylename="Cd Black Italic" filename="../instances/AktivGroteskCd_BlkIt.ufo" postscriptfontname="AktivGrotesk-CdBlackItalic" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black" familyname="Aktiv Grotesk" stylename="Ex Black" filename="../instances/AktivGroteskEx_Blk.ufo" postscriptfontname="AktivGrotesk-ExBlack" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black Italic" familyname="Aktiv Grotesk" stylename="Ex Black Italic" filename="../instances/AktivGroteskEx_BlkIt.ufo" postscriptfontname="AktivGrotesk-ExBlackItalic" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Italic.designspace b/Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Italic.designspace
new file mode 100644
index 00000000..adfe245d
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Italic.designspace
@@ -0,0 +1,326 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20"/>
+ </axes>
+ <sources>
+ <source filename="../caption/master_0/SourceSerif-Italic_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_1/SourceSerif-Italic_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_2/SourceSerif-Italic_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../text/master_0/SourceSerif-Italic_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_1/SourceSerif-Italic_1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_2/SourceSerif-Italic_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../display/master_0/SourceSerif-Italic_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_1/SourceSerif-Italic_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_2/SourceSerif-Italic_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Source Serif 4 Caption ExtraLight Italic" familyname="Source Serif 4" stylename="Caption ExtraLight Italic" filename="Source Serif 4-Caption ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionExtraLight" stylemapfamilyname="Source Serif 4 Caption ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Light Italic" familyname="Source Serif 4" stylename="Caption Light Italic" filename="Source Serif 4-Caption Light Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionLight" stylemapfamilyname="Source Serif 4 Caption Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Italic" familyname="Source Serif 4" stylename="Caption Italic" filename="Source Serif 4-Caption Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionRegular" stylemapfamilyname="Source Serif 4 Caption Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Semibold Italic" familyname="Source Serif 4" stylename="Caption Semibold Italic" filename="Source Serif 4-Caption Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionSemibold" stylemapfamilyname="Source Serif 4 Caption Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Bold Italic" familyname="Source Serif 4" stylename="Caption Bold Italic" filename="Source Serif 4-Caption Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionBold" stylemapfamilyname="Source Serif 4 Caption Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Black Italic" familyname="Source Serif 4" stylename="Caption Black Italic" filename="Source Serif 4-Caption Black Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionBlack" stylemapfamilyname="Source Serif 4 Caption Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText ExtraLight Italic" familyname="Source Serif 4" stylename="SmText ExtraLight Italic" filename="Source Serif 4-SmText ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextExtraLight" stylemapfamilyname="Source Serif 4 SmallText ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Light Italic" familyname="Source Serif 4" stylename="SmText Light Italic" filename="Source Serif 4-SmText Light Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextLight" stylemapfamilyname="Source Serif 4 SmallText Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Italic" familyname="Source Serif 4" stylename="SmText Italic" filename="Source Serif 4-SmText Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextRegular" stylemapfamilyname="Source Serif 4 SmallText Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Semibold Italic" familyname="Source Serif 4" stylename="SmText Semibold Italic" filename="Source Serif 4-SmText Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextSemibold" stylemapfamilyname="Source Serif 4 SmallText Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Bold Italic" familyname="Source Serif 4" stylename="SmText Bold Italic" filename="Source Serif 4-SmText Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextBold" stylemapfamilyname="Source Serif 4 SmallText Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Black Italic" familyname="Source Serif 4" stylename="SmText Black Italic" filename="Source Serif 4-SmText Black Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextBlack" stylemapfamilyname="Source Serif 4 SmallText Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 ExtraLight Italic" familyname="Source Serif 4" stylename="ExtraLight Italic" filename="Source Serif 4-ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-ExtraLight" stylemapfamilyname="Source Serif 4 ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Light Italic" familyname="Source Serif 4" stylename="Light Italic" filename="Source Serif 4-Light Italic.ttf" postscriptfontname="SourceSerif4Italic-Light" stylemapfamilyname="Source Serif 4 Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Italic" familyname="Source Serif 4" stylename="Italic" filename="Source Serif 4-Italic.ttf" postscriptfontname="SourceSerif4Italic-Regular" stylemapfamilyname="Source Serif 4 Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Semibold Italic" familyname="Source Serif 4" stylename="Semibold Italic" filename="Source Serif 4-Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-Semibold" stylemapfamilyname="Source Serif 4 Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Bold Italic" familyname="Source Serif 4" stylename="Bold Italic" filename="Source Serif 4-Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-Bold" stylemapfamilyname="Source Serif 4 Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Black Italic" familyname="Source Serif 4" stylename="Black Italic" filename="Source Serif 4-Black Italic.ttf" postscriptfontname="SourceSerif4Italic-Black" stylemapfamilyname="Source Serif 4 Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead ExtraLight Italic" familyname="Source Serif 4" stylename="Subhead ExtraLight Italic" filename="Source Serif 4-Subhead ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadExtraLight" stylemapfamilyname="Source Serif 4 Subhead ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Light Italic" familyname="Source Serif 4" stylename="Subhead Light Italic" filename="Source Serif 4-Subhead Light Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadLight" stylemapfamilyname="Source Serif 4 Subhead Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Italic" familyname="Source Serif 4" stylename="Subhead Italic" filename="Source Serif 4-Subhead Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadRegular" stylemapfamilyname="Source Serif 4 Subhead Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Semibold Italic" familyname="Source Serif 4" stylename="Subhead Semibold Italic" filename="Source Serif 4-Subhead Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadSemibold" stylemapfamilyname="Source Serif 4 Subhead Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Bold Italic" familyname="Source Serif 4" stylename="Subhead Bold Italic" filename="Source Serif 4-Subhead Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadBold" stylemapfamilyname="Source Serif 4 Subhead Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Black Italic" familyname="Source Serif 4" stylename="Subhead Black Italic" filename="Source Serif 4-Subhead Black Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadBlack" stylemapfamilyname="Source Serif 4 Subhead Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display ExtraLight Italic" familyname="Source Serif 4" stylename="Display ExtraLight Italic" filename="Source Serif 4-Display ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayExtraLight" stylemapfamilyname="Source Serif 4 Display ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Light Italic" familyname="Source Serif 4" stylename="Display Light Italic" filename="Source Serif 4-Display Light Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayLight" stylemapfamilyname="Source Serif 4 Display Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Italic" familyname="Source Serif 4" stylename="Display Italic" filename="Source Serif 4-Display Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayRegular" stylemapfamilyname="Source Serif 4 Display Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Semibold Italic" familyname="Source Serif 4" stylename="Display Semibold Italic" filename="Source Serif 4-Display Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplaySemibold" stylemapfamilyname="Source Serif 4 Display Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Bold Italic" familyname="Source Serif 4" stylename="Display Bold Italic" filename="Source Serif 4-Display Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayBold" stylemapfamilyname="Source Serif 4 Display Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Black Italic" familyname="Source Serif 4" stylename="Display Black Italic" filename="Source Serif 4-Display Black Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayBlack" stylemapfamilyname="Source Serif 4 Display Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>f.liga</string>
+ <string>f.ligalong</string>
+ <string>tonos.cap</string>
+ <string>dieresiscmb.tight</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Roman.designspace b/Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Roman.designspace
new file mode 100644
index 00000000..30f181e3
--- /dev/null
+++ b/Tests/designspaceLib/data/convert5to4_output/SourceSerif4Variable-Roman.designspace
@@ -0,0 +1,334 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="4.1">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20"/>
+ </axes>
+ <sources>
+ <source filename="../caption/master_0/SourceSerif_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_1/SourceSerif_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_2/SourceSerif_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../text/master_0/SourceSerif_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_1/SourceSerif_1.ufo" familyname="Source Serif 4">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_2/SourceSerif_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../display/master_0/SourceSerif_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_1/SourceSerif_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_2/SourceSerif_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Source Serif 4 Caption ExtraLight" familyname="Source Serif 4" stylename="Caption ExtraLight" filename="Source Serif 4-Caption ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-CaptionExtraLight" stylemapfamilyname="Source Serif 4 Caption ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Light" familyname="Source Serif 4" stylename="Caption Light" filename="Source Serif 4-Caption Light.ttf" postscriptfontname="SourceSerif4Roman-CaptionLight" stylemapfamilyname="Source Serif 4 Caption Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption" familyname="Source Serif 4" stylename="Caption" filename="Source Serif 4-Caption.ttf" postscriptfontname="SourceSerif4Roman-CaptionRegular" stylemapfamilyname="Source Serif 4 Caption" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Semibold" familyname="Source Serif 4" stylename="Caption Semibold" filename="Source Serif 4-Caption Semibold.ttf" postscriptfontname="SourceSerif4Roman-CaptionSemibold" stylemapfamilyname="Source Serif 4 Caption Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Bold" familyname="Source Serif 4" stylename="Caption Bold" filename="Source Serif 4-Caption Bold.ttf" postscriptfontname="SourceSerif4Roman-CaptionBold" stylemapfamilyname="Source Serif 4 Caption Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Caption Black" familyname="Source Serif 4" stylename="Caption Black" filename="Source Serif 4-Caption Black.ttf" postscriptfontname="SourceSerif4Roman-CaptionBlack" stylemapfamilyname="Source Serif 4 Caption Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText ExtraLight" familyname="Source Serif 4" stylename="SmText ExtraLight" filename="Source Serif 4-SmText ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-SmTextExtraLight" stylemapfamilyname="Source Serif 4 SmallText ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Light" familyname="Source Serif 4" stylename="SmText Light" filename="Source Serif 4-SmText Light.ttf" postscriptfontname="SourceSerif4Roman-SmTextLight" stylemapfamilyname="Source Serif 4 SmallText Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText" familyname="Source Serif 4" stylename="SmText" filename="Source Serif 4-SmText.ttf" postscriptfontname="SourceSerif4Roman-SmTextRegular" stylemapfamilyname="Source Serif 4 SmallText" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Semibold" familyname="Source Serif 4" stylename="SmText Semibold" filename="Source Serif 4-SmText Semibold.ttf" postscriptfontname="SourceSerif4Roman-SmTextSemibold" stylemapfamilyname="Source Serif 4 SmallText Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Bold" familyname="Source Serif 4" stylename="SmText Bold" filename="Source Serif 4-SmText Bold.ttf" postscriptfontname="SourceSerif4Roman-SmTextBold" stylemapfamilyname="Source Serif 4 SmallText Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 SmText Black" familyname="Source Serif 4" stylename="SmText Black" filename="Source Serif 4-SmText Black.ttf" postscriptfontname="SourceSerif4Roman-SmTextBlack" stylemapfamilyname="Source Serif 4 SmallText Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 ExtraLight" familyname="Source Serif 4" stylename="ExtraLight" filename="Source Serif 4-ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-ExtraLight" stylemapfamilyname="Source Serif 4 ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Light" familyname="Source Serif 4" stylename="Light" filename="Source Serif 4-Light.ttf" postscriptfontname="SourceSerif4Roman-Light" stylemapfamilyname="Source Serif 4 Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Regular" familyname="Source Serif 4" stylename="Regular" filename="Source Serif 4-Regular.ttf" postscriptfontname="SourceSerif4Roman-Regular" stylemapfamilyname="Source Serif 4" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Semibold" familyname="Source Serif 4" stylename="Semibold" filename="Source Serif 4-Semibold.ttf" postscriptfontname="SourceSerif4Roman-Semibold" stylemapfamilyname="Source Serif 4 Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Bold" familyname="Source Serif 4" stylename="Bold" filename="Source Serif 4-Bold.ttf" postscriptfontname="SourceSerif4Roman-Bold" stylemapfamilyname="Source Serif 4 Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Black" familyname="Source Serif 4" stylename="Black" filename="Source Serif 4-Black.ttf" postscriptfontname="SourceSerif4Roman-Black" stylemapfamilyname="Source Serif 4 Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead ExtraLight" familyname="Source Serif 4" stylename="Subhead ExtraLight" filename="Source Serif 4-Subhead ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-SubheadExtraLight" stylemapfamilyname="Source Serif 4 Subhead ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Light" familyname="Source Serif 4" stylename="Subhead Light" filename="Source Serif 4-Subhead Light.ttf" postscriptfontname="SourceSerif4Roman-SubheadLight" stylemapfamilyname="Source Serif 4 Subhead Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead" familyname="Source Serif 4" stylename="Subhead" filename="Source Serif 4-Subhead.ttf" postscriptfontname="SourceSerif4Roman-SubheadRegular" stylemapfamilyname="Source Serif 4 Subhead" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Semibold" familyname="Source Serif 4" stylename="Subhead Semibold" filename="Source Serif 4-Subhead Semibold.ttf" postscriptfontname="SourceSerif4Roman-SubheadSemibold" stylemapfamilyname="Source Serif 4 Subhead Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Bold" familyname="Source Serif 4" stylename="Subhead Bold" filename="Source Serif 4-Subhead Bold.ttf" postscriptfontname="SourceSerif4Roman-SubheadBold" stylemapfamilyname="Source Serif 4 Subhead Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Subhead Black" familyname="Source Serif 4" stylename="Subhead Black" filename="Source Serif 4-Subhead Black.ttf" postscriptfontname="SourceSerif4Roman-SubheadBlack" stylemapfamilyname="Source Serif 4 Subhead Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display ExtraLight" familyname="Source Serif 4" stylename="Display ExtraLight" filename="Source Serif 4-Display ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-DisplayExtraLight" stylemapfamilyname="Source Serif 4 Display ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Light" familyname="Source Serif 4" stylename="Display Light" filename="Source Serif 4-Display Light.ttf" postscriptfontname="SourceSerif4Roman-DisplayLight" stylemapfamilyname="Source Serif 4 Display Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display" familyname="Source Serif 4" stylename="Display" filename="Source Serif 4-Display.ttf" postscriptfontname="SourceSerif4Roman-DisplayRegular" stylemapfamilyname="Source Serif 4 Display" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Semibold" familyname="Source Serif 4" stylename="Display Semibold" filename="Source Serif 4-Display Semibold.ttf" postscriptfontname="SourceSerif4Roman-DisplaySemibold" stylemapfamilyname="Source Serif 4 Display Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Bold" familyname="Source Serif 4" stylename="Display Bold" filename="Source Serif 4-Display Bold.ttf" postscriptfontname="SourceSerif4Roman-DisplayBold" stylemapfamilyname="Source Serif 4 Display Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ <instance name="Source Serif 4 Display Black" familyname="Source Serif 4" stylename="Display Black" filename="Source Serif 4-Display Black.ttf" postscriptfontname="SourceSerif4Roman-DisplayBlack" stylemapfamilyname="Source Serif 4 Display Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ <kerning/>
+ <info/>
+ </instance>
+ </instances>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>tonos.cap</string>
+ <string>f.ligalong</string>
+ <string>dieresiscmb.tight</string>
+ <string>IJ</string>
+ <string>Tbar</string>
+ <string>colontriangularmod</string>
+ <string>crossmark</string>
+ <string>ij</string>
+ <string>overline</string>
+ <string>similar</string>
+ <string>tbar</string>
+ <string>triangularbullet</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_Wght.designspace b/Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_Wght.designspace
new file mode 100644
index 00000000..dc38cd7d
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_Wght.designspace
@@ -0,0 +1,96 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_WghtWdth.designspace b/Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_WghtWdth.designspace
new file mode 100644
index 00000000..8cda9140
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/AktivGroteskVF_Italics_WghtWdth.designspace
@@ -0,0 +1,262 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_HairIt.ufo" name="Aktiv Grotesk Cd Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_HairIt.ufo" name="Aktiv Grotesk Ex Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_It.ufo" name="Aktiv Grotesk Cd Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_It.ufo" name="Aktiv Grotesk Ex Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_BlkIt.ufo" name="Aktiv Grotesk Cd Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_BlkIt.ufo" name="Aktiv Grotesk Ex Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair Italic" familyname="Aktiv Grotesk" stylename="Cd Hair Italic" filename="../instances/AktivGroteskCd_HairIt.ufo" postscriptfontname="AktivGrotesk-CdHairItalic" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair Italic" familyname="Aktiv Grotesk" stylename="Ex Hair Italic" filename="../instances/AktivGroteskEx_HairIt.ufo" postscriptfontname="AktivGrotesk-ExHairItalic" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin Italic" familyname="Aktiv Grotesk" stylename="Cd Thin Italic" filename="../instances/AktivGroteskCd_ThIt.ufo" postscriptfontname="AktivGrotesk-CdThinItalic" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin Italic" familyname="Aktiv Grotesk" stylename="Ex Thin Italic" filename="../instances/AktivGroteskEx_ThIt.ufo" postscriptfontname="AktivGrotesk-ExThinItalic" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light Italic" familyname="Aktiv Grotesk" stylename="Cd Light Italic" filename="../instances/AktivGroteskCd_LtIt.ufo" postscriptfontname="AktivGrotesk-CdLightItalic" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light Italic" familyname="Aktiv Grotesk" stylename="Ex Light Italic" filename="../instances/AktivGroteskEx_LtIt.ufo" postscriptfontname="AktivGrotesk-ExLightItalic" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Italic" familyname="Aktiv Grotesk" stylename="Cd Italic" filename="../instances/AktivGroteskCd_It.ufo" postscriptfontname="AktivGrotesk-CdItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Italic" familyname="Aktiv Grotesk" stylename="Ex Italic" filename="../instances/AktivGroteskEx_It.ufo" postscriptfontname="AktivGrotesk-ExItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium Italic" familyname="Aktiv Grotesk" stylename="Cd Medium Italic" filename="../instances/AktivGroteskCd_MdIt.ufo" postscriptfontname="AktivGrotesk-CdMediumItalic" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium Italic" familyname="Aktiv Grotesk" stylename="Ex Medium Italic" filename="../instances/AktivGroteskEx_MdIt.ufo" postscriptfontname="AktivGrotesk-ExMediumItalic" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold Italic" familyname="Aktiv Grotesk" stylename="Cd SemiBold Italic" filename="../../build/instances/AktivGroteskCd_SBdIt.ufo" postscriptfontname="AktivGrotesk-CdSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold Italic" familyname="Aktiv Grotesk" stylename="Ex SemiBold Italic" filename="../../build/instances/AktivGroteskEx_SBdIt.ufo" postscriptfontname="AktivGrotesk-ExSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold Italic" familyname="Aktiv Grotesk" stylename="Cd Bold Italic" filename="../instances/AktivGroteskCd_BdIt.ufo" postscriptfontname="AktivGrotesk-CdBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold Italic" familyname="Aktiv Grotesk" stylename="Ex Bold Italic" filename="../instances/AktivGroteskEx_BdIt.ufo" postscriptfontname="AktivGrotesk-ExBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold Italic" familyname="Aktiv Grotesk" stylename="Cd XBold Italic" filename="../instances/AktivGroteskCd_XBdIt.ufo" postscriptfontname="AktivGrotesk-CdXBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold Italic" familyname="Aktiv Grotesk" stylename="Ex XBold Italic" filename="../instances/AktivGroteskEx_XBdIt.ufo" postscriptfontname="AktivGrotesk-ExXBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black Italic" familyname="Aktiv Grotesk" stylename="Cd Black Italic" filename="../instances/AktivGroteskCd_BlkIt.ufo" postscriptfontname="AktivGrotesk-CdBlackItalic" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black Italic" familyname="Aktiv Grotesk" stylename="Ex Black Italic" filename="../instances/AktivGroteskEx_BlkIt.ufo" postscriptfontname="AktivGrotesk-ExBlackItalic" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/AktivGroteskVF_Wght.designspace b/Tests/designspaceLib/data/split_output/AktivGroteskVF_Wght.designspace
new file mode 100644
index 00000000..db621655
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/AktivGroteskVF_Wght.designspace
@@ -0,0 +1,85 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ </axes>
+ <sources>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdth.designspace b/Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdth.designspace
new file mode 100644
index 00000000..113c6897
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdth.designspace
@@ -0,0 +1,253 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_Hair.ufo" name="Aktiv Grotesk Cd Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Hair.ufo" name="Aktiv Grotesk Ex Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Rg.ufo" name="Aktiv Grotesk Cd">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Rg.ufo" name="Aktiv Grotesk Ex">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Blk.ufo" name="Aktiv Grotesk Cd Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Blk.ufo" name="Aktiv Grotesk Ex Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair" familyname="Aktiv Grotesk" stylename="Cd Hair" filename="../instances/AktivGroteskCd_Hair.ufo" postscriptfontname="AktivGrotesk-CdHair" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair" familyname="Aktiv Grotesk" stylename="Ex Hair" filename="../instances/AktivGroteskEx_Hair.ufo" postscriptfontname="AktivGrotesk-ExHair" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin" familyname="Aktiv Grotesk" stylename="Cd Thin" filename="../instances/AktivGroteskCd_Th.ufo" postscriptfontname="AktivGrotesk-CdThin" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin" familyname="Aktiv Grotesk" stylename="Ex Thin" filename="../instances/AktivGroteskEx_Th.ufo" postscriptfontname="AktivGrotesk-ExThin" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light" familyname="Aktiv Grotesk" stylename="Cd Light" filename="../instances/AktivGroteskCd_Lt.ufo" postscriptfontname="AktivGrotesk-CdLight" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light" familyname="Aktiv Grotesk" stylename="Ex Light" filename="../instances/AktivGroteskEx_Lt.ufo" postscriptfontname="AktivGrotesk-ExLight" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd" familyname="Aktiv Grotesk" stylename="Cd" filename="../instances/AktivGroteskCd_Rg.ufo" postscriptfontname="AktivGrotesk-Cd" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex" familyname="Aktiv Grotesk" stylename="Ex" filename="../instances/AktivGroteskEx_Rg.ufo" postscriptfontname="AktivGrotesk-Ex" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium" familyname="Aktiv Grotesk" stylename="Cd Medium" filename="../instances/AktivGroteskCd_Md.ufo" postscriptfontname="AktivGrotesk-CdMedium" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium" familyname="Aktiv Grotesk" stylename="Ex Medium" filename="../instances/AktivGroteskEx_Md.ufo" postscriptfontname="AktivGrotesk-ExMedium" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold" familyname="Aktiv Grotesk" stylename="Cd SemiBold" filename="../../build/instances/AktivGroteskCd_SBd.ufo" postscriptfontname="AktivGrotesk-CdSemiBold" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold" familyname="Aktiv Grotesk" stylename="Ex SemiBold" filename="../../build/instances/AktivGroteskEx_SBd.ufo" postscriptfontname="AktivGrotesk-ExSemiBold" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold" familyname="Aktiv Grotesk" stylename="Cd Bold" filename="../instances/AktivGroteskCd_Bd.ufo" postscriptfontname="AktivGrotesk-CdBold" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold" familyname="Aktiv Grotesk" stylename="Ex Bold" filename="../instances/AktivGroteskEx_Bd.ufo" postscriptfontname="AktivGrotesk-ExBold" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold" familyname="Aktiv Grotesk" stylename="Cd XBold" filename="../instances/AktivGroteskCd_XBd.ufo" postscriptfontname="AktivGrotesk-CdXBold" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold" familyname="Aktiv Grotesk" stylename="Ex XBold" filename="../instances/AktivGroteskEx_XBd.ufo" postscriptfontname="AktivGrotesk-ExXBold" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black" familyname="Aktiv Grotesk" stylename="Cd Black" filename="../instances/AktivGroteskCd_Blk.ufo" postscriptfontname="AktivGrotesk-CdBlack" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black" familyname="Aktiv Grotesk" stylename="Ex Black" filename="../instances/AktivGroteskEx_Blk.ufo" postscriptfontname="AktivGrotesk-ExBlack" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdthItal.designspace b/Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdthItal.designspace
new file mode 100644
index 00000000..4a1ef48f
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/AktivGroteskVF_WghtWdthItal.designspace
@@ -0,0 +1,562 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ <axis tag="ital" name="Italic" minimum="0" maximum="1" default="0"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <conditionset>
+ <condition name="Italic" minimum="0.1" maximum="1"/>
+ </conditionset>
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_Hair.ufo" name="Aktiv Grotesk Cd Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_HairIt.ufo" name="Aktiv Grotesk Cd Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Hair.ufo" name="Aktiv Grotesk Ex Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_HairIt.ufo" name="Aktiv Grotesk Ex Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Rg.ufo" name="Aktiv Grotesk Cd">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_It.ufo" name="Aktiv Grotesk Cd Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Rg.ufo" name="Aktiv Grotesk Ex">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_It.ufo" name="Aktiv Grotesk Ex Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Blk.ufo" name="Aktiv Grotesk Cd Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_BlkIt.ufo" name="Aktiv Grotesk Cd Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Blk.ufo" name="Aktiv Grotesk Ex Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_BlkIt.ufo" name="Aktiv Grotesk Ex Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair" familyname="Aktiv Grotesk" stylename="Cd Hair" filename="../instances/AktivGroteskCd_Hair.ufo" postscriptfontname="AktivGrotesk-CdHair" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Hair Italic" familyname="Aktiv Grotesk" stylename="Cd Hair Italic" filename="../instances/AktivGroteskCd_HairIt.ufo" postscriptfontname="AktivGrotesk-CdHairItalic" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair" familyname="Aktiv Grotesk" stylename="Ex Hair" filename="../instances/AktivGroteskEx_Hair.ufo" postscriptfontname="AktivGrotesk-ExHair" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair Italic" familyname="Aktiv Grotesk" stylename="Ex Hair Italic" filename="../instances/AktivGroteskEx_HairIt.ufo" postscriptfontname="AktivGrotesk-ExHairItalic" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin" familyname="Aktiv Grotesk" stylename="Cd Thin" filename="../instances/AktivGroteskCd_Th.ufo" postscriptfontname="AktivGrotesk-CdThin" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin Italic" familyname="Aktiv Grotesk" stylename="Cd Thin Italic" filename="../instances/AktivGroteskCd_ThIt.ufo" postscriptfontname="AktivGrotesk-CdThinItalic" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin" familyname="Aktiv Grotesk" stylename="Ex Thin" filename="../instances/AktivGroteskEx_Th.ufo" postscriptfontname="AktivGrotesk-ExThin" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin Italic" familyname="Aktiv Grotesk" stylename="Ex Thin Italic" filename="../instances/AktivGroteskEx_ThIt.ufo" postscriptfontname="AktivGrotesk-ExThinItalic" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light" familyname="Aktiv Grotesk" stylename="Cd Light" filename="../instances/AktivGroteskCd_Lt.ufo" postscriptfontname="AktivGrotesk-CdLight" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light Italic" familyname="Aktiv Grotesk" stylename="Cd Light Italic" filename="../instances/AktivGroteskCd_LtIt.ufo" postscriptfontname="AktivGrotesk-CdLightItalic" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light" familyname="Aktiv Grotesk" stylename="Ex Light" filename="../instances/AktivGroteskEx_Lt.ufo" postscriptfontname="AktivGrotesk-ExLight" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light Italic" familyname="Aktiv Grotesk" stylename="Ex Light Italic" filename="../instances/AktivGroteskEx_LtIt.ufo" postscriptfontname="AktivGrotesk-ExLightItalic" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd" familyname="Aktiv Grotesk" stylename="Cd" filename="../instances/AktivGroteskCd_Rg.ufo" postscriptfontname="AktivGrotesk-Cd" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Italic" familyname="Aktiv Grotesk" stylename="Cd Italic" filename="../instances/AktivGroteskCd_It.ufo" postscriptfontname="AktivGrotesk-CdItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex" familyname="Aktiv Grotesk" stylename="Ex" filename="../instances/AktivGroteskEx_Rg.ufo" postscriptfontname="AktivGrotesk-Ex" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Italic" familyname="Aktiv Grotesk" stylename="Ex Italic" filename="../instances/AktivGroteskEx_It.ufo" postscriptfontname="AktivGrotesk-ExItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium" familyname="Aktiv Grotesk" stylename="Cd Medium" filename="../instances/AktivGroteskCd_Md.ufo" postscriptfontname="AktivGrotesk-CdMedium" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium Italic" familyname="Aktiv Grotesk" stylename="Cd Medium Italic" filename="../instances/AktivGroteskCd_MdIt.ufo" postscriptfontname="AktivGrotesk-CdMediumItalic" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium" familyname="Aktiv Grotesk" stylename="Ex Medium" filename="../instances/AktivGroteskEx_Md.ufo" postscriptfontname="AktivGrotesk-ExMedium" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium Italic" familyname="Aktiv Grotesk" stylename="Ex Medium Italic" filename="../instances/AktivGroteskEx_MdIt.ufo" postscriptfontname="AktivGrotesk-ExMediumItalic" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold" familyname="Aktiv Grotesk" stylename="Cd SemiBold" filename="../../build/instances/AktivGroteskCd_SBd.ufo" postscriptfontname="AktivGrotesk-CdSemiBold" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold Italic" familyname="Aktiv Grotesk" stylename="Cd SemiBold Italic" filename="../../build/instances/AktivGroteskCd_SBdIt.ufo" postscriptfontname="AktivGrotesk-CdSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold" familyname="Aktiv Grotesk" stylename="Ex SemiBold" filename="../../build/instances/AktivGroteskEx_SBd.ufo" postscriptfontname="AktivGrotesk-ExSemiBold" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold Italic" familyname="Aktiv Grotesk" stylename="Ex SemiBold Italic" filename="../../build/instances/AktivGroteskEx_SBdIt.ufo" postscriptfontname="AktivGrotesk-ExSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold" familyname="Aktiv Grotesk" stylename="Cd Bold" filename="../instances/AktivGroteskCd_Bd.ufo" postscriptfontname="AktivGrotesk-CdBold" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold Italic" familyname="Aktiv Grotesk" stylename="Cd Bold Italic" filename="../instances/AktivGroteskCd_BdIt.ufo" postscriptfontname="AktivGrotesk-CdBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold" familyname="Aktiv Grotesk" stylename="Ex Bold" filename="../instances/AktivGroteskEx_Bd.ufo" postscriptfontname="AktivGrotesk-ExBold" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold Italic" familyname="Aktiv Grotesk" stylename="Ex Bold Italic" filename="../instances/AktivGroteskEx_BdIt.ufo" postscriptfontname="AktivGrotesk-ExBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold" familyname="Aktiv Grotesk" stylename="Cd XBold" filename="../instances/AktivGroteskCd_XBd.ufo" postscriptfontname="AktivGrotesk-CdXBold" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold Italic" familyname="Aktiv Grotesk" stylename="Cd XBold Italic" filename="../instances/AktivGroteskCd_XBdIt.ufo" postscriptfontname="AktivGrotesk-CdXBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold" familyname="Aktiv Grotesk" stylename="Ex XBold" filename="../instances/AktivGroteskEx_XBd.ufo" postscriptfontname="AktivGrotesk-ExXBold" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold Italic" familyname="Aktiv Grotesk" stylename="Ex XBold Italic" filename="../instances/AktivGroteskEx_XBdIt.ufo" postscriptfontname="AktivGrotesk-ExXBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black" familyname="Aktiv Grotesk" stylename="Cd Black" filename="../instances/AktivGroteskCd_Blk.ufo" postscriptfontname="AktivGrotesk-CdBlack" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black Italic" familyname="Aktiv Grotesk" stylename="Cd Black Italic" filename="../instances/AktivGroteskCd_BlkIt.ufo" postscriptfontname="AktivGrotesk-CdBlackItalic" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black" familyname="Aktiv Grotesk" stylename="Ex Black" filename="../instances/AktivGroteskEx_Blk.ufo" postscriptfontname="AktivGrotesk-ExBlack" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black Italic" familyname="Aktiv Grotesk" stylename="Ex Black Italic" filename="../instances/AktivGroteskEx_BlkIt.ufo" postscriptfontname="AktivGrotesk-ExBlackItalic" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight.designspace b/Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight.designspace
new file mode 100644
index 00000000..6dcc6002
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight.designspace
@@ -0,0 +1,55 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="300" maximum="700" default="300">
+ <map input="300" output="0"/>
+ <map input="500" output="500"/>
+ <map input="700" output="1000"/>
+ </axis>
+ </axes>
+ <rules>
+ <rule name="fold_I_serifs">
+ <sub name="I" with="I.narrow"/>
+ </rule>
+ <rule name="fold_S_terminals">
+ <conditionset>
+ <condition name="weight" minimum="0" maximum="500"/>
+ </conditionset>
+ <sub name="S" with="S.closed"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../MutatorSansLightCondensed.ufo" name="master.MutatorMathTest.LightCondensed.0" familyname="MutatorMathTest" stylename="LightCondensed">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansBoldCondensed.ufo" name="master.MutatorMathTest.BoldCondensed.1" familyname="MutatorMathTest" stylename="BoldCondensed">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.crossbar" layer="support.crossbar">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold Condensed" familyname="MutatorMathTest" stylename="Sans Bold Condensed" filename="MutatorMathTest-Sans Bold Condensed.ttf" postscriptfontname="MutatorMathTest-SansBoldCondensed" stylemapfamilyname="MutatorMathTest Sans Bold Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight_Width.designspace b/Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight_Width.designspace
new file mode 100644
index 00000000..e83be97f
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/MutatorSansVariable_Weight_Width.designspace
@@ -0,0 +1,129 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="300" maximum="700" default="300">
+ <map input="300" output="0"/>
+ <map input="500" output="500"/>
+ <map input="700" output="1000"/>
+ </axis>
+ <axis tag="wdth" name="width" minimum="50" maximum="200" default="50">
+ <map input="50" output="0"/>
+ <map input="100" output="500"/>
+ <map input="200" output="1000"/>
+ </axis>
+ </axes>
+ <rules>
+ <rule name="fold_I_serifs">
+ <conditionset>
+ <condition name="width" minimum="0" maximum="328"/>
+ </conditionset>
+ <sub name="I" with="I.narrow"/>
+ </rule>
+ <rule name="fold_S_terminals">
+ <conditionset>
+ <condition name="weight" minimum="0" maximum="500"/>
+ </conditionset>
+ <sub name="S" with="S.closed"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../MutatorSansLightCondensed.ufo" name="master.MutatorMathTest.LightCondensed.0" familyname="MutatorMathTest" stylename="LightCondensed">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansBoldCondensed.ufo" name="master.MutatorMathTest.BoldCondensed.1" familyname="MutatorMathTest" stylename="BoldCondensed">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightWide.ufo" name="master.MutatorMathTest.LightWide.2" familyname="MutatorMathTest" stylename="LightWide">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansBoldWide.ufo" name="master.MutatorMathTest.BoldWide.3" familyname="MutatorMathTest" stylename="BoldWide">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.crossbar" layer="support.crossbar">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.S.wide" layer="support.S.wide">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.S.middle" layer="support.S.middle">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ <dimension name="width" xvalue="569.078"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold Condensed" familyname="MutatorMathTest" stylename="Sans Bold Condensed" filename="MutatorMathTest-Sans Bold Condensed.ttf" postscriptfontname="MutatorMathTest-SansBoldCondensed" stylemapfamilyname="MutatorMathTest Sans Bold Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Extended" familyname="MutatorMathTest" stylename="Sans Light Extended" filename="MutatorMathTest-Sans Light Extended.ttf" postscriptfontname="MutatorMathTest-SansLightExtended" stylemapfamilyname="MutatorMathTest Sans Light Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold Extended" familyname="MutatorMathTest" stylename="Sans Bold Extended" filename="MutatorMathTest-Sans Bold Extended.ttf" postscriptfontname="MutatorMathTest-SansBoldExtended" stylemapfamilyname="MutatorMathTest Sans Bold Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Medium" familyname="MutatorMathTest" stylename="Sans Medium" filename="MutatorMathTest-Sans Medium.ttf" postscriptfontname="MutatorMathTest-SansMedium" stylemapfamilyname="MutatorMathTest Sans Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="327"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Medium" familyname="MutatorMathTest" stylename="Sans Medium" filename="MutatorMathTest-Sans Medium.ttf" postscriptfontname="MutatorMathTest-SansMedium" stylemapfamilyname="MutatorMathTest Sans Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="327"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold" familyname="MutatorMathTest" stylename="Sans Bold" filename="MutatorMathTest-Sans Bold.ttf" postscriptfontname="MutatorMathTest-SansBold" stylemapfamilyname="MutatorMathTest Sans Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="569.078"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Medium Extended" familyname="MutatorMathTest" stylename="Sans Medium Extended" filename="MutatorMathTest-Sans Medium Extended.ttf" postscriptfontname="MutatorMathTest-SansMediumExtended" stylemapfamilyname="MutatorMathTest Sans Medium Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/MutatorSansVariable_Width.designspace b/Tests/designspaceLib/data/split_output/MutatorSansVariable_Width.designspace
new file mode 100644
index 00000000..9384c73c
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/MutatorSansVariable_Width.designspace
@@ -0,0 +1,50 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wdth" name="width" minimum="50" maximum="200" default="50">
+ <map input="50" output="0"/>
+ <map input="100" output="500"/>
+ <map input="200" output="1000"/>
+ </axis>
+ </axes>
+ <rules>
+ <rule name="fold_I_serifs">
+ <conditionset>
+ <condition name="width" minimum="0" maximum="328"/>
+ </conditionset>
+ <sub name="I" with="I.narrow"/>
+ </rule>
+ <rule name="fold_S_terminals">
+ <sub name="S" with="S.closed"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../MutatorSansLightCondensed.ufo" name="master.MutatorMathTest.LightCondensed.0" familyname="MutatorMathTest" stylename="LightCondensed">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightWide.ufo" name="master.MutatorMathTest.LightWide.2" familyname="MutatorMathTest" stylename="LightWide">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Extended" familyname="MutatorMathTest" stylename="Sans Light Extended" filename="MutatorMathTest-Sans Light Extended.ttf" postscriptfontname="MutatorMathTest-SansLightExtended" stylemapfamilyname="MutatorMathTest Sans Light Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/MutatorSerifVariable_Width.designspace b/Tests/designspaceLib/data/split_output/MutatorSerifVariable_Width.designspace
new file mode 100644
index 00000000..b71025a2
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/MutatorSerifVariable_Width.designspace
@@ -0,0 +1,29 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wdth" name="width" minimum="50" maximum="200" default="50">
+ <map input="50" output="0"/>
+ <map input="100" output="500"/>
+ <map input="200" output="1000"/>
+ </axis>
+ </axes>
+ <sources>
+ <source filename="../MutatorSerifLightCondensed.ufo" familyname="MutatorMathTest" stylename="SerifLightCondensed">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSerifLightWide.ufo" familyname="MutatorMathTest" stylename="SerifLightWide">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="MutatorMathTest Serif Light Condensed" familyname="MutatorMathTest" stylename="Serif Light Condensed" filename="MutatorMathTest-Serif Light Condensed.ttf" postscriptfontname="MutatorMathTest-SerifLightCondensed" stylemapfamilyname="MutatorMathTest Serif Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/SourceSerif4Variable-Italic.designspace b/Tests/designspaceLib/data/split_output/SourceSerif4Variable-Italic.designspace
new file mode 100644
index 00000000..c1eb95d9
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/SourceSerif4Variable-Italic.designspace
@@ -0,0 +1,266 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20"/>
+ </axes>
+ <sources>
+ <source filename="../caption/master_0/SourceSerif-Italic_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_1/SourceSerif-Italic_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_2/SourceSerif-Italic_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../text/master_0/SourceSerif-Italic_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_1/SourceSerif-Italic_1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_2/SourceSerif-Italic_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../display/master_0/SourceSerif-Italic_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_1/SourceSerif-Italic_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_2/SourceSerif-Italic_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Source Serif 4 Caption ExtraLight Italic" familyname="Source Serif 4" stylename="Caption ExtraLight Italic" filename="Source Serif 4-Caption ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionExtraLight" stylemapfamilyname="Source Serif 4 Caption ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Light Italic" familyname="Source Serif 4" stylename="Caption Light Italic" filename="Source Serif 4-Caption Light Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionLight" stylemapfamilyname="Source Serif 4 Caption Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Italic" familyname="Source Serif 4" stylename="Caption Italic" filename="Source Serif 4-Caption Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionRegular" stylemapfamilyname="Source Serif 4 Caption Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Semibold Italic" familyname="Source Serif 4" stylename="Caption Semibold Italic" filename="Source Serif 4-Caption Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionSemibold" stylemapfamilyname="Source Serif 4 Caption Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Bold Italic" familyname="Source Serif 4" stylename="Caption Bold Italic" filename="Source Serif 4-Caption Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionBold" stylemapfamilyname="Source Serif 4 Caption Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Black Italic" familyname="Source Serif 4" stylename="Caption Black Italic" filename="Source Serif 4-Caption Black Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionBlack" stylemapfamilyname="Source Serif 4 Caption Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText ExtraLight Italic" familyname="Source Serif 4" stylename="SmText ExtraLight Italic" filename="Source Serif 4-SmText ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextExtraLight" stylemapfamilyname="Source Serif 4 SmallText ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Light Italic" familyname="Source Serif 4" stylename="SmText Light Italic" filename="Source Serif 4-SmText Light Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextLight" stylemapfamilyname="Source Serif 4 SmallText Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Italic" familyname="Source Serif 4" stylename="SmText Italic" filename="Source Serif 4-SmText Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextRegular" stylemapfamilyname="Source Serif 4 SmallText Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Semibold Italic" familyname="Source Serif 4" stylename="SmText Semibold Italic" filename="Source Serif 4-SmText Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextSemibold" stylemapfamilyname="Source Serif 4 SmallText Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Bold Italic" familyname="Source Serif 4" stylename="SmText Bold Italic" filename="Source Serif 4-SmText Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextBold" stylemapfamilyname="Source Serif 4 SmallText Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Black Italic" familyname="Source Serif 4" stylename="SmText Black Italic" filename="Source Serif 4-SmText Black Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextBlack" stylemapfamilyname="Source Serif 4 SmallText Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 ExtraLight Italic" familyname="Source Serif 4" stylename="ExtraLight Italic" filename="Source Serif 4-ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-ExtraLight" stylemapfamilyname="Source Serif 4 ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Light Italic" familyname="Source Serif 4" stylename="Light Italic" filename="Source Serif 4-Light Italic.ttf" postscriptfontname="SourceSerif4Italic-Light" stylemapfamilyname="Source Serif 4 Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Italic" familyname="Source Serif 4" stylename="Italic" filename="Source Serif 4-Italic.ttf" postscriptfontname="SourceSerif4Italic-Regular" stylemapfamilyname="Source Serif 4 Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Semibold Italic" familyname="Source Serif 4" stylename="Semibold Italic" filename="Source Serif 4-Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-Semibold" stylemapfamilyname="Source Serif 4 Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Bold Italic" familyname="Source Serif 4" stylename="Bold Italic" filename="Source Serif 4-Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-Bold" stylemapfamilyname="Source Serif 4 Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Black Italic" familyname="Source Serif 4" stylename="Black Italic" filename="Source Serif 4-Black Italic.ttf" postscriptfontname="SourceSerif4Italic-Black" stylemapfamilyname="Source Serif 4 Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead ExtraLight Italic" familyname="Source Serif 4" stylename="Subhead ExtraLight Italic" filename="Source Serif 4-Subhead ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadExtraLight" stylemapfamilyname="Source Serif 4 Subhead ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Light Italic" familyname="Source Serif 4" stylename="Subhead Light Italic" filename="Source Serif 4-Subhead Light Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadLight" stylemapfamilyname="Source Serif 4 Subhead Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Italic" familyname="Source Serif 4" stylename="Subhead Italic" filename="Source Serif 4-Subhead Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadRegular" stylemapfamilyname="Source Serif 4 Subhead Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Semibold Italic" familyname="Source Serif 4" stylename="Subhead Semibold Italic" filename="Source Serif 4-Subhead Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadSemibold" stylemapfamilyname="Source Serif 4 Subhead Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Bold Italic" familyname="Source Serif 4" stylename="Subhead Bold Italic" filename="Source Serif 4-Subhead Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadBold" stylemapfamilyname="Source Serif 4 Subhead Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Black Italic" familyname="Source Serif 4" stylename="Subhead Black Italic" filename="Source Serif 4-Subhead Black Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadBlack" stylemapfamilyname="Source Serif 4 Subhead Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display ExtraLight Italic" familyname="Source Serif 4" stylename="Display ExtraLight Italic" filename="Source Serif 4-Display ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayExtraLight" stylemapfamilyname="Source Serif 4 Display ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Light Italic" familyname="Source Serif 4" stylename="Display Light Italic" filename="Source Serif 4-Display Light Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayLight" stylemapfamilyname="Source Serif 4 Display Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Italic" familyname="Source Serif 4" stylename="Display Italic" filename="Source Serif 4-Display Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayRegular" stylemapfamilyname="Source Serif 4 Display Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Semibold Italic" familyname="Source Serif 4" stylename="Display Semibold Italic" filename="Source Serif 4-Display Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplaySemibold" stylemapfamilyname="Source Serif 4 Display Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Bold Italic" familyname="Source Serif 4" stylename="Display Bold Italic" filename="Source Serif 4-Display Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayBold" stylemapfamilyname="Source Serif 4 Display Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Black Italic" familyname="Source Serif 4" stylename="Display Black Italic" filename="Source Serif 4-Display Black Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayBlack" stylemapfamilyname="Source Serif 4 Display Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ </instances>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>f.liga</string>
+ <string>f.ligalong</string>
+ <string>tonos.cap</string>
+ <string>dieresiscmb.tight</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/SourceSerif4Variable-Roman.designspace b/Tests/designspaceLib/data/split_output/SourceSerif4Variable-Roman.designspace
new file mode 100644
index 00000000..1eee53e0
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/SourceSerif4Variable-Roman.designspace
@@ -0,0 +1,274 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20"/>
+ </axes>
+ <sources>
+ <source filename="../caption/master_0/SourceSerif_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_1/SourceSerif_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_2/SourceSerif_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../text/master_0/SourceSerif_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_1/SourceSerif_1.ufo" familyname="Source Serif 4">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_2/SourceSerif_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../display/master_0/SourceSerif_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_1/SourceSerif_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_2/SourceSerif_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="Source Serif 4 Caption ExtraLight" familyname="Source Serif 4" stylename="Caption ExtraLight" filename="Source Serif 4-Caption ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-CaptionExtraLight" stylemapfamilyname="Source Serif 4 Caption ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Light" familyname="Source Serif 4" stylename="Caption Light" filename="Source Serif 4-Caption Light.ttf" postscriptfontname="SourceSerif4Roman-CaptionLight" stylemapfamilyname="Source Serif 4 Caption Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption" familyname="Source Serif 4" stylename="Caption" filename="Source Serif 4-Caption.ttf" postscriptfontname="SourceSerif4Roman-CaptionRegular" stylemapfamilyname="Source Serif 4 Caption" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Semibold" familyname="Source Serif 4" stylename="Caption Semibold" filename="Source Serif 4-Caption Semibold.ttf" postscriptfontname="SourceSerif4Roman-CaptionSemibold" stylemapfamilyname="Source Serif 4 Caption Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Bold" familyname="Source Serif 4" stylename="Caption Bold" filename="Source Serif 4-Caption Bold.ttf" postscriptfontname="SourceSerif4Roman-CaptionBold" stylemapfamilyname="Source Serif 4 Caption Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Black" familyname="Source Serif 4" stylename="Caption Black" filename="Source Serif 4-Caption Black.ttf" postscriptfontname="SourceSerif4Roman-CaptionBlack" stylemapfamilyname="Source Serif 4 Caption Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText ExtraLight" familyname="Source Serif 4" stylename="SmText ExtraLight" filename="Source Serif 4-SmText ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-SmTextExtraLight" stylemapfamilyname="Source Serif 4 SmallText ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Light" familyname="Source Serif 4" stylename="SmText Light" filename="Source Serif 4-SmText Light.ttf" postscriptfontname="SourceSerif4Roman-SmTextLight" stylemapfamilyname="Source Serif 4 SmallText Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText" familyname="Source Serif 4" stylename="SmText" filename="Source Serif 4-SmText.ttf" postscriptfontname="SourceSerif4Roman-SmTextRegular" stylemapfamilyname="Source Serif 4 SmallText" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Semibold" familyname="Source Serif 4" stylename="SmText Semibold" filename="Source Serif 4-SmText Semibold.ttf" postscriptfontname="SourceSerif4Roman-SmTextSemibold" stylemapfamilyname="Source Serif 4 SmallText Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Bold" familyname="Source Serif 4" stylename="SmText Bold" filename="Source Serif 4-SmText Bold.ttf" postscriptfontname="SourceSerif4Roman-SmTextBold" stylemapfamilyname="Source Serif 4 SmallText Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Black" familyname="Source Serif 4" stylename="SmText Black" filename="Source Serif 4-SmText Black.ttf" postscriptfontname="SourceSerif4Roman-SmTextBlack" stylemapfamilyname="Source Serif 4 SmallText Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 ExtraLight" familyname="Source Serif 4" stylename="ExtraLight" filename="Source Serif 4-ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-ExtraLight" stylemapfamilyname="Source Serif 4 ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Light" familyname="Source Serif 4" stylename="Light" filename="Source Serif 4-Light.ttf" postscriptfontname="SourceSerif4Roman-Light" stylemapfamilyname="Source Serif 4 Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Regular" familyname="Source Serif 4" stylename="Regular" filename="Source Serif 4-Regular.ttf" postscriptfontname="SourceSerif4Roman-Regular" stylemapfamilyname="Source Serif 4" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Semibold" familyname="Source Serif 4" stylename="Semibold" filename="Source Serif 4-Semibold.ttf" postscriptfontname="SourceSerif4Roman-Semibold" stylemapfamilyname="Source Serif 4 Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Bold" familyname="Source Serif 4" stylename="Bold" filename="Source Serif 4-Bold.ttf" postscriptfontname="SourceSerif4Roman-Bold" stylemapfamilyname="Source Serif 4 Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Black" familyname="Source Serif 4" stylename="Black" filename="Source Serif 4-Black.ttf" postscriptfontname="SourceSerif4Roman-Black" stylemapfamilyname="Source Serif 4 Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead ExtraLight" familyname="Source Serif 4" stylename="Subhead ExtraLight" filename="Source Serif 4-Subhead ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-SubheadExtraLight" stylemapfamilyname="Source Serif 4 Subhead ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Light" familyname="Source Serif 4" stylename="Subhead Light" filename="Source Serif 4-Subhead Light.ttf" postscriptfontname="SourceSerif4Roman-SubheadLight" stylemapfamilyname="Source Serif 4 Subhead Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead" familyname="Source Serif 4" stylename="Subhead" filename="Source Serif 4-Subhead.ttf" postscriptfontname="SourceSerif4Roman-SubheadRegular" stylemapfamilyname="Source Serif 4 Subhead" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Semibold" familyname="Source Serif 4" stylename="Subhead Semibold" filename="Source Serif 4-Subhead Semibold.ttf" postscriptfontname="SourceSerif4Roman-SubheadSemibold" stylemapfamilyname="Source Serif 4 Subhead Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Bold" familyname="Source Serif 4" stylename="Subhead Bold" filename="Source Serif 4-Subhead Bold.ttf" postscriptfontname="SourceSerif4Roman-SubheadBold" stylemapfamilyname="Source Serif 4 Subhead Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Black" familyname="Source Serif 4" stylename="Subhead Black" filename="Source Serif 4-Subhead Black.ttf" postscriptfontname="SourceSerif4Roman-SubheadBlack" stylemapfamilyname="Source Serif 4 Subhead Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display ExtraLight" familyname="Source Serif 4" stylename="Display ExtraLight" filename="Source Serif 4-Display ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-DisplayExtraLight" stylemapfamilyname="Source Serif 4 Display ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Light" familyname="Source Serif 4" stylename="Display Light" filename="Source Serif 4-Display Light.ttf" postscriptfontname="SourceSerif4Roman-DisplayLight" stylemapfamilyname="Source Serif 4 Display Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display" familyname="Source Serif 4" stylename="Display" filename="Source Serif 4-Display.ttf" postscriptfontname="SourceSerif4Roman-DisplayRegular" stylemapfamilyname="Source Serif 4 Display" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Semibold" familyname="Source Serif 4" stylename="Display Semibold" filename="Source Serif 4-Display Semibold.ttf" postscriptfontname="SourceSerif4Roman-DisplaySemibold" stylemapfamilyname="Source Serif 4 Display Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Bold" familyname="Source Serif 4" stylename="Display Bold" filename="Source Serif 4-Display Bold.ttf" postscriptfontname="SourceSerif4Roman-DisplayBold" stylemapfamilyname="Source Serif 4 Display Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Black" familyname="Source Serif 4" stylename="Display Black" filename="Source Serif 4-Display Black.ttf" postscriptfontname="SourceSerif4Roman-DisplayBlack" stylemapfamilyname="Source Serif 4 Display Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ </instances>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>tonos.cap</string>
+ <string>f.ligalong</string>
+ <string>dieresiscmb.tight</string>
+ <string>IJ</string>
+ <string>Tbar</string>
+ <string>colontriangularmod</string>
+ <string>crossmark</string>
+ <string>ij</string>
+ <string>overline</string>
+ <string>similar</string>
+ <string>tbar</string>
+ <string>triangularbullet</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_0.0.designspace b/Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_0.0.designspace
new file mode 100644
index 00000000..d811ffa0
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_0.0.designspace
@@ -0,0 +1,147 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="300" maximum="700" default="300">
+ <map input="300" output="0"/>
+ <map input="500" output="500"/>
+ <map input="700" output="1000"/>
+ </axis>
+ <axis tag="wdth" name="width" minimum="50" maximum="200" default="50">
+ <map input="50" output="0"/>
+ <map input="100" output="500"/>
+ <map input="200" output="1000"/>
+ </axis>
+ </axes>
+ <rules>
+ <rule name="fold_I_serifs">
+ <conditionset>
+ <condition name="width" minimum="0" maximum="328"/>
+ </conditionset>
+ <sub name="I" with="I.narrow"/>
+ </rule>
+ <rule name="fold_S_terminals">
+ <conditionset>
+ <condition name="weight" minimum="0" maximum="500"/>
+ </conditionset>
+ <sub name="S" with="S.closed"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../MutatorSansLightCondensed.ufo" name="master.MutatorMathTest.LightCondensed.0" familyname="MutatorMathTest" stylename="LightCondensed">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansBoldCondensed.ufo" name="master.MutatorMathTest.BoldCondensed.1" familyname="MutatorMathTest" stylename="BoldCondensed">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightWide.ufo" name="master.MutatorMathTest.LightWide.2" familyname="MutatorMathTest" stylename="LightWide">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansBoldWide.ufo" name="master.MutatorMathTest.BoldWide.3" familyname="MutatorMathTest" stylename="BoldWide">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.crossbar" layer="support.crossbar">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.S.wide" layer="support.S.wide">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="../MutatorSansLightCondensed.ufo" name="support.S.middle" layer="support.S.middle">
+ <location>
+ <dimension name="weight" xvalue="700"/>
+ <dimension name="width" xvalue="569.078"/>
+ </location>
+ </source>
+ </sources>
+ <variable-fonts>
+ <variable-font name="MutatorSansVariable_Weight_Width">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ <axis-subset name="width"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="MutatorSansVariable_Weight">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="MutatorSansVariable_Width">
+ <axis-subsets>
+ <axis-subset name="width"/>
+ </axis-subsets>
+ </variable-font>
+ </variable-fonts>
+ <instances>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold Condensed" familyname="MutatorMathTest" stylename="Sans Bold Condensed" filename="MutatorMathTest-Sans Bold Condensed.ttf" postscriptfontname="MutatorMathTest-SansBoldCondensed" stylemapfamilyname="MutatorMathTest Sans Bold Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Extended" familyname="MutatorMathTest" stylename="Sans Light Extended" filename="MutatorMathTest-Sans Light Extended.ttf" postscriptfontname="MutatorMathTest-SansLightExtended" stylemapfamilyname="MutatorMathTest Sans Light Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold Extended" familyname="MutatorMathTest" stylename="Sans Bold Extended" filename="MutatorMathTest-Sans Bold Extended.ttf" postscriptfontname="MutatorMathTest-SansBoldExtended" stylemapfamilyname="MutatorMathTest Sans Bold Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Medium" familyname="MutatorMathTest" stylename="Sans Medium" filename="MutatorMathTest-Sans Medium.ttf" postscriptfontname="MutatorMathTest-SansMedium" stylemapfamilyname="MutatorMathTest Sans Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="327"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Medium" familyname="MutatorMathTest" stylename="Sans Medium" filename="MutatorMathTest-Sans Medium.ttf" postscriptfontname="MutatorMathTest-SansMedium" stylemapfamilyname="MutatorMathTest Sans Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="327"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Bold" familyname="MutatorMathTest" stylename="Sans Bold" filename="MutatorMathTest-Sans Bold.ttf" postscriptfontname="MutatorMathTest-SansBold" stylemapfamilyname="MutatorMathTest Sans Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="width" xvalue="569.078"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Medium Extended" familyname="MutatorMathTest" stylename="Sans Medium Extended" filename="MutatorMathTest-Sans Medium Extended.ttf" postscriptfontname="MutatorMathTest-SansMediumExtended" stylemapfamilyname="MutatorMathTest Sans Medium Extended" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance name="MutatorMathTest Sans Light Condensed" familyname="MutatorMathTest" stylename="Sans Light Condensed" filename="MutatorMathTest-Sans Light Condensed.ttf" postscriptfontname="MutatorMathTest-SansLightCondensed" stylemapfamilyname="MutatorMathTest Sans Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_1.0.designspace b/Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_1.0.designspace
new file mode 100644
index 00000000..5e3a6f52
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/test_v5_MutatorSans_and_Serif_serif_1.0.designspace
@@ -0,0 +1,44 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="300" maximum="700" default="300">
+ <map input="300" output="0"/>
+ <map input="500" output="500"/>
+ <map input="700" output="1000"/>
+ </axis>
+ <axis tag="wdth" name="width" minimum="50" maximum="200" default="50">
+ <map input="50" output="0"/>
+ <map input="100" output="500"/>
+ <map input="200" output="1000"/>
+ </axis>
+ </axes>
+ <sources>
+ <source filename="../MutatorSerifLightCondensed.ufo" familyname="MutatorMathTest" stylename="SerifLightCondensed">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../MutatorSerifLightWide.ufo" familyname="MutatorMathTest" stylename="SerifLightWide">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="1000"/>
+ </location>
+ </source>
+ </sources>
+ <variable-fonts>
+ <variable-font name="MutatorSerifVariable_Width">
+ <axis-subsets>
+ <axis-subset name="width"/>
+ </axis-subsets>
+ </variable-font>
+ </variable-fonts>
+ <instances>
+ <instance name="MutatorMathTest Serif Light Condensed" familyname="MutatorMathTest" stylename="Serif Light Condensed" filename="MutatorMathTest-Serif Light Condensed.ttf" postscriptfontname="MutatorMathTest-SerifLightCondensed" stylemapfamilyname="MutatorMathTest Serif Light Condensed" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="width" xvalue="0"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/test_v5_aktiv_.designspace b/Tests/designspaceLib/data/split_output/test_v5_aktiv_.designspace
new file mode 100644
index 00000000..4451e6e6
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/test_v5_aktiv_.designspace
@@ -0,0 +1,595 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100"/>
+ <axis tag="ital" name="Italic" minimum="0" maximum="1" default="0"/>
+ </axes>
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <conditionset>
+ <condition name="Italic" minimum="0.1" maximum="1"/>
+ </conditionset>
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="../AktivGroteskCd_Hair.ufo" name="Aktiv Grotesk Cd Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_HairIt.ufo" name="Aktiv Grotesk Cd Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Hair.ufo" name="Aktiv Grotesk Ex Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_HairIt.ufo" name="Aktiv Grotesk Ex Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Rg.ufo" name="Aktiv Grotesk Cd">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_It.ufo" name="Aktiv Grotesk Cd Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Rg.ufo" name="Aktiv Grotesk Ex">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_It.ufo" name="Aktiv Grotesk Ex Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_Blk.ufo" name="Aktiv Grotesk Cd Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskCd_BlkIt.ufo" name="Aktiv Grotesk Cd Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_Blk.ufo" name="Aktiv Grotesk Ex Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="../AktivGroteskEx_BlkIt.ufo" name="Aktiv Grotesk Ex Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+ <variable-fonts>
+ <variable-font name="AktivGroteskVF_WghtWdthItal">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ <axis-subset name="Italic"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_WghtWdth">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_Wght">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_Italics_WghtWdth">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ <axis-subset name="Italic" uservalue="1"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_Italics_Wght">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Italic" uservalue="1"/>
+ </axis-subsets>
+ </variable-font>
+ </variable-fonts>
+ <instances>
+ <instance name="Aktiv Grotesk Cd Hair" familyname="Aktiv Grotesk" stylename="Cd Hair" filename="../instances/AktivGroteskCd_Hair.ufo" postscriptfontname="AktivGrotesk-CdHair" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Hair Italic" familyname="Aktiv Grotesk" stylename="Cd Hair Italic" filename="../instances/AktivGroteskCd_HairIt.ufo" postscriptfontname="AktivGrotesk-CdHairItalic" stylemapfamilyname="Aktiv Grotesk Cd Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Hair" familyname="Aktiv Grotesk" stylename="Hair" filename="../instances/AktivGrotesk_Hair.ufo" postscriptfontname="AktivGrotesk-Hair" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Hair Italic" familyname="Aktiv Grotesk" stylename="Hair Italic" filename="../instances/AktivGrotesk_HairIt.ufo" postscriptfontname="AktivGrotesk-HairItalic" stylemapfamilyname="Aktiv Grotesk Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair" familyname="Aktiv Grotesk" stylename="Ex Hair" filename="../instances/AktivGroteskEx_Hair.ufo" postscriptfontname="AktivGrotesk-ExHair" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Hair Italic" familyname="Aktiv Grotesk" stylename="Ex Hair Italic" filename="../instances/AktivGroteskEx_HairIt.ufo" postscriptfontname="AktivGrotesk-ExHairItalic" stylemapfamilyname="Aktiv Grotesk Ex Hair" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin" familyname="Aktiv Grotesk" stylename="Cd Thin" filename="../instances/AktivGroteskCd_Th.ufo" postscriptfontname="AktivGrotesk-CdThin" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Thin Italic" familyname="Aktiv Grotesk" stylename="Cd Thin Italic" filename="../instances/AktivGroteskCd_ThIt.ufo" postscriptfontname="AktivGrotesk-CdThinItalic" stylemapfamilyname="Aktiv Grotesk Cd Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin" familyname="Aktiv Grotesk" stylename="Thin" filename="../instances/AktivGrotesk_Th.ufo" postscriptfontname="AktivGrotesk-Thin" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Thin Italic" familyname="Aktiv Grotesk" stylename="Thin Italic" filename="../instances/AktivGrotesk_ThIt.ufo" postscriptfontname="AktivGrotesk-ThinItalic" stylemapfamilyname="Aktiv Grotesk Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin" familyname="Aktiv Grotesk" stylename="Ex Thin" filename="../instances/AktivGroteskEx_Th.ufo" postscriptfontname="AktivGrotesk-ExThin" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Thin Italic" familyname="Aktiv Grotesk" stylename="Ex Thin Italic" filename="../instances/AktivGroteskEx_ThIt.ufo" postscriptfontname="AktivGrotesk-ExThinItalic" stylemapfamilyname="Aktiv Grotesk Ex Thin" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light" familyname="Aktiv Grotesk" stylename="Cd Light" filename="../instances/AktivGroteskCd_Lt.ufo" postscriptfontname="AktivGrotesk-CdLight" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Light Italic" familyname="Aktiv Grotesk" stylename="Cd Light Italic" filename="../instances/AktivGroteskCd_LtIt.ufo" postscriptfontname="AktivGrotesk-CdLightItalic" stylemapfamilyname="Aktiv Grotesk Cd Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light" familyname="Aktiv Grotesk" stylename="Light" filename="../instances/AktivGrotesk_Lt.ufo" postscriptfontname="AktivGrotesk-Light" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Light Italic" familyname="Aktiv Grotesk" stylename="Light Italic" filename="../instances/AktivGrotesk_LtIt.ufo" postscriptfontname="AktivGrotesk-LightItalic" stylemapfamilyname="Aktiv Grotesk Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light" familyname="Aktiv Grotesk" stylename="Ex Light" filename="../instances/AktivGroteskEx_Lt.ufo" postscriptfontname="AktivGrotesk-ExLight" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Light Italic" familyname="Aktiv Grotesk" stylename="Ex Light Italic" filename="../instances/AktivGroteskEx_LtIt.ufo" postscriptfontname="AktivGrotesk-ExLightItalic" stylemapfamilyname="Aktiv Grotesk Ex Light" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd" familyname="Aktiv Grotesk" stylename="Cd" filename="../instances/AktivGroteskCd_Rg.ufo" postscriptfontname="AktivGrotesk-Cd" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Italic" familyname="Aktiv Grotesk" stylename="Cd Italic" filename="../instances/AktivGroteskCd_It.ufo" postscriptfontname="AktivGrotesk-CdItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk " familyname="Aktiv Grotesk" stylename="" filename="../instances/AktivGrotesk_Rg.ufo" postscriptfontname="AktivGrotesk-" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Italic" familyname="Aktiv Grotesk" stylename="Italic" filename="../instances/AktivGrotesk_It.ufo" postscriptfontname="AktivGrotesk-Italic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex" familyname="Aktiv Grotesk" stylename="Ex" filename="../instances/AktivGroteskEx_Rg.ufo" postscriptfontname="AktivGrotesk-Ex" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Italic" familyname="Aktiv Grotesk" stylename="Ex Italic" filename="../instances/AktivGroteskEx_It.ufo" postscriptfontname="AktivGrotesk-ExItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium" familyname="Aktiv Grotesk" stylename="Cd Medium" filename="../instances/AktivGroteskCd_Md.ufo" postscriptfontname="AktivGrotesk-CdMedium" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Medium Italic" familyname="Aktiv Grotesk" stylename="Cd Medium Italic" filename="../instances/AktivGroteskCd_MdIt.ufo" postscriptfontname="AktivGrotesk-CdMediumItalic" stylemapfamilyname="Aktiv Grotesk Cd Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium" familyname="Aktiv Grotesk" stylename="Medium" filename="../instances/AktivGrotesk_Md.ufo" postscriptfontname="AktivGrotesk-Medium" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Medium Italic" familyname="Aktiv Grotesk" stylename="Medium Italic" filename="../instances/AktivGrotesk_MdIt.ufo" postscriptfontname="AktivGrotesk-MediumItalic" stylemapfamilyname="Aktiv Grotesk Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium" familyname="Aktiv Grotesk" stylename="Ex Medium" filename="../instances/AktivGroteskEx_Md.ufo" postscriptfontname="AktivGrotesk-ExMedium" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Medium Italic" familyname="Aktiv Grotesk" stylename="Ex Medium Italic" filename="../instances/AktivGroteskEx_MdIt.ufo" postscriptfontname="AktivGrotesk-ExMediumItalic" stylemapfamilyname="Aktiv Grotesk Ex Medium" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold" familyname="Aktiv Grotesk" stylename="Cd SemiBold" filename="../../build/instances/AktivGroteskCd_SBd.ufo" postscriptfontname="AktivGrotesk-CdSemiBold" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd SemiBold Italic" familyname="Aktiv Grotesk" stylename="Cd SemiBold Italic" filename="../../build/instances/AktivGroteskCd_SBdIt.ufo" postscriptfontname="AktivGrotesk-CdSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold" familyname="Aktiv Grotesk" stylename="SemiBold" filename="../../build/instances/AktivGrotesk_SBd.ufo" postscriptfontname="AktivGrotesk-SemiBold" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk SemiBold Italic" familyname="Aktiv Grotesk" stylename="SemiBold Italic" filename="../../build/instances/AktivGrotesk_SBdIt.ufo" postscriptfontname="AktivGrotesk-SemiBoldItalic" stylemapfamilyname="Aktiv Grotesk SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold" familyname="Aktiv Grotesk" stylename="Ex SemiBold" filename="../../build/instances/AktivGroteskEx_SBd.ufo" postscriptfontname="AktivGrotesk-ExSemiBold" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex SemiBold Italic" familyname="Aktiv Grotesk" stylename="Ex SemiBold Italic" filename="../../build/instances/AktivGroteskEx_SBdIt.ufo" postscriptfontname="AktivGrotesk-ExSemiBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex SemiBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold" familyname="Aktiv Grotesk" stylename="Cd Bold" filename="../instances/AktivGroteskCd_Bd.ufo" postscriptfontname="AktivGrotesk-CdBold" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Bold Italic" familyname="Aktiv Grotesk" stylename="Cd Bold Italic" filename="../instances/AktivGroteskCd_BdIt.ufo" postscriptfontname="AktivGrotesk-CdBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold" familyname="Aktiv Grotesk" stylename="Bold" filename="../instances/AktivGrotesk_Bd.ufo" postscriptfontname="AktivGrotesk-Bold" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Bold Italic" familyname="Aktiv Grotesk" stylename="Bold Italic" filename="../instances/AktivGrotesk_BdIt.ufo" postscriptfontname="AktivGrotesk-BoldItalic" stylemapfamilyname="Aktiv Grotesk" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold" familyname="Aktiv Grotesk" stylename="Ex Bold" filename="../instances/AktivGroteskEx_Bd.ufo" postscriptfontname="AktivGrotesk-ExBold" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Bold Italic" familyname="Aktiv Grotesk" stylename="Ex Bold Italic" filename="../instances/AktivGroteskEx_BdIt.ufo" postscriptfontname="AktivGrotesk-ExBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex" stylemapstylename="bold italic">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold" familyname="Aktiv Grotesk" stylename="Cd XBold" filename="../instances/AktivGroteskCd_XBd.ufo" postscriptfontname="AktivGrotesk-CdXBold" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd XBold Italic" familyname="Aktiv Grotesk" stylename="Cd XBold Italic" filename="../instances/AktivGroteskCd_XBdIt.ufo" postscriptfontname="AktivGrotesk-CdXBoldItalic" stylemapfamilyname="Aktiv Grotesk Cd XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold" familyname="Aktiv Grotesk" stylename="XBold" filename="../instances/AktivGrotesk_XBd.ufo" postscriptfontname="AktivGrotesk-XBold" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk XBold Italic" familyname="Aktiv Grotesk" stylename="XBold Italic" filename="../instances/AktivGrotesk_XBdIt.ufo" postscriptfontname="AktivGrotesk-XBoldItalic" stylemapfamilyname="Aktiv Grotesk XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold" familyname="Aktiv Grotesk" stylename="Ex XBold" filename="../instances/AktivGroteskEx_XBd.ufo" postscriptfontname="AktivGrotesk-ExXBold" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex XBold Italic" familyname="Aktiv Grotesk" stylename="Ex XBold Italic" filename="../instances/AktivGroteskEx_XBdIt.ufo" postscriptfontname="AktivGrotesk-ExXBoldItalic" stylemapfamilyname="Aktiv Grotesk Ex XBold" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black" familyname="Aktiv Grotesk" stylename="Cd Black" filename="../instances/AktivGroteskCd_Blk.ufo" postscriptfontname="AktivGrotesk-CdBlack" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Cd Black Italic" familyname="Aktiv Grotesk" stylename="Cd Black Italic" filename="../instances/AktivGroteskCd_BlkIt.ufo" postscriptfontname="AktivGrotesk-CdBlackItalic" stylemapfamilyname="Aktiv Grotesk Cd Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black" familyname="Aktiv Grotesk" stylename="Black" filename="../instances/AktivGrotesk_Blk.ufo" postscriptfontname="AktivGrotesk-Black" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Black Italic" familyname="Aktiv Grotesk" stylename="Black Italic" filename="../instances/AktivGrotesk_BlkIt.ufo" postscriptfontname="AktivGrotesk-BlackItalic" stylemapfamilyname="Aktiv Grotesk Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black" familyname="Aktiv Grotesk" stylename="Ex Black" filename="../instances/AktivGroteskEx_Blk.ufo" postscriptfontname="AktivGrotesk-ExBlack" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="regular">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance name="Aktiv Grotesk Ex Black Italic" familyname="Aktiv Grotesk" stylename="Ex Black Italic" filename="../instances/AktivGroteskEx_BlkIt.ufo" postscriptfontname="AktivGrotesk-ExBlackItalic" stylemapfamilyname="Aktiv Grotesk Ex Black" stylemapstylename="italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_0.0.designspace b/Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_0.0.designspace
new file mode 100644
index 00000000..447461fb
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_0.0.designspace
@@ -0,0 +1,282 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20"/>
+ </axes>
+ <sources>
+ <source filename="../caption/master_0/SourceSerif_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_1/SourceSerif_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_2/SourceSerif_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../text/master_0/SourceSerif_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_1/SourceSerif_1.ufo" familyname="Source Serif 4">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_2/SourceSerif_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../display/master_0/SourceSerif_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_1/SourceSerif_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_2/SourceSerif_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ </sources>
+ <variable-fonts>
+ <variable-font name="SourceSerif4Variable-Roman">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ <axis-subset name="optical"/>
+ </axis-subsets>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>tonos.cap</string>
+ <string>f.ligalong</string>
+ <string>dieresiscmb.tight</string>
+ <string>IJ</string>
+ <string>Tbar</string>
+ <string>colontriangularmod</string>
+ <string>crossmark</string>
+ <string>ij</string>
+ <string>overline</string>
+ <string>similar</string>
+ <string>tbar</string>
+ <string>triangularbullet</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+ </variable-font>
+ </variable-fonts>
+ <instances>
+ <instance name="Source Serif 4 Caption ExtraLight" familyname="Source Serif 4" stylename="Caption ExtraLight" filename="Source Serif 4-Caption ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-CaptionExtraLight" stylemapfamilyname="Source Serif 4 Caption ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Light" familyname="Source Serif 4" stylename="Caption Light" filename="Source Serif 4-Caption Light.ttf" postscriptfontname="SourceSerif4Roman-CaptionLight" stylemapfamilyname="Source Serif 4 Caption Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption" familyname="Source Serif 4" stylename="Caption" filename="Source Serif 4-Caption.ttf" postscriptfontname="SourceSerif4Roman-CaptionRegular" stylemapfamilyname="Source Serif 4 Caption" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Semibold" familyname="Source Serif 4" stylename="Caption Semibold" filename="Source Serif 4-Caption Semibold.ttf" postscriptfontname="SourceSerif4Roman-CaptionSemibold" stylemapfamilyname="Source Serif 4 Caption Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Bold" familyname="Source Serif 4" stylename="Caption Bold" filename="Source Serif 4-Caption Bold.ttf" postscriptfontname="SourceSerif4Roman-CaptionBold" stylemapfamilyname="Source Serif 4 Caption Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Black" familyname="Source Serif 4" stylename="Caption Black" filename="Source Serif 4-Caption Black.ttf" postscriptfontname="SourceSerif4Roman-CaptionBlack" stylemapfamilyname="Source Serif 4 Caption Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText ExtraLight" familyname="Source Serif 4" stylename="SmText ExtraLight" filename="Source Serif 4-SmText ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-SmTextExtraLight" stylemapfamilyname="Source Serif 4 SmallText ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Light" familyname="Source Serif 4" stylename="SmText Light" filename="Source Serif 4-SmText Light.ttf" postscriptfontname="SourceSerif4Roman-SmTextLight" stylemapfamilyname="Source Serif 4 SmallText Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText" familyname="Source Serif 4" stylename="SmText" filename="Source Serif 4-SmText.ttf" postscriptfontname="SourceSerif4Roman-SmTextRegular" stylemapfamilyname="Source Serif 4 SmallText" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Semibold" familyname="Source Serif 4" stylename="SmText Semibold" filename="Source Serif 4-SmText Semibold.ttf" postscriptfontname="SourceSerif4Roman-SmTextSemibold" stylemapfamilyname="Source Serif 4 SmallText Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Bold" familyname="Source Serif 4" stylename="SmText Bold" filename="Source Serif 4-SmText Bold.ttf" postscriptfontname="SourceSerif4Roman-SmTextBold" stylemapfamilyname="Source Serif 4 SmallText Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Black" familyname="Source Serif 4" stylename="SmText Black" filename="Source Serif 4-SmText Black.ttf" postscriptfontname="SourceSerif4Roman-SmTextBlack" stylemapfamilyname="Source Serif 4 SmallText Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 ExtraLight" familyname="Source Serif 4" stylename="ExtraLight" filename="Source Serif 4-ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-ExtraLight" stylemapfamilyname="Source Serif 4 ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Light" familyname="Source Serif 4" stylename="Light" filename="Source Serif 4-Light.ttf" postscriptfontname="SourceSerif4Roman-Light" stylemapfamilyname="Source Serif 4 Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Regular" familyname="Source Serif 4" stylename="Regular" filename="Source Serif 4-Regular.ttf" postscriptfontname="SourceSerif4Roman-Regular" stylemapfamilyname="Source Serif 4" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Semibold" familyname="Source Serif 4" stylename="Semibold" filename="Source Serif 4-Semibold.ttf" postscriptfontname="SourceSerif4Roman-Semibold" stylemapfamilyname="Source Serif 4 Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Bold" familyname="Source Serif 4" stylename="Bold" filename="Source Serif 4-Bold.ttf" postscriptfontname="SourceSerif4Roman-Bold" stylemapfamilyname="Source Serif 4 Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Black" familyname="Source Serif 4" stylename="Black" filename="Source Serif 4-Black.ttf" postscriptfontname="SourceSerif4Roman-Black" stylemapfamilyname="Source Serif 4 Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead ExtraLight" familyname="Source Serif 4" stylename="Subhead ExtraLight" filename="Source Serif 4-Subhead ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-SubheadExtraLight" stylemapfamilyname="Source Serif 4 Subhead ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Light" familyname="Source Serif 4" stylename="Subhead Light" filename="Source Serif 4-Subhead Light.ttf" postscriptfontname="SourceSerif4Roman-SubheadLight" stylemapfamilyname="Source Serif 4 Subhead Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead" familyname="Source Serif 4" stylename="Subhead" filename="Source Serif 4-Subhead.ttf" postscriptfontname="SourceSerif4Roman-SubheadRegular" stylemapfamilyname="Source Serif 4 Subhead" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Semibold" familyname="Source Serif 4" stylename="Subhead Semibold" filename="Source Serif 4-Subhead Semibold.ttf" postscriptfontname="SourceSerif4Roman-SubheadSemibold" stylemapfamilyname="Source Serif 4 Subhead Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Bold" familyname="Source Serif 4" stylename="Subhead Bold" filename="Source Serif 4-Subhead Bold.ttf" postscriptfontname="SourceSerif4Roman-SubheadBold" stylemapfamilyname="Source Serif 4 Subhead Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Black" familyname="Source Serif 4" stylename="Subhead Black" filename="Source Serif 4-Subhead Black.ttf" postscriptfontname="SourceSerif4Roman-SubheadBlack" stylemapfamilyname="Source Serif 4 Subhead Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display ExtraLight" familyname="Source Serif 4" stylename="Display ExtraLight" filename="Source Serif 4-Display ExtraLight.ttf" postscriptfontname="SourceSerif4Roman-DisplayExtraLight" stylemapfamilyname="Source Serif 4 Display ExtraLight" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Light" familyname="Source Serif 4" stylename="Display Light" filename="Source Serif 4-Display Light.ttf" postscriptfontname="SourceSerif4Roman-DisplayLight" stylemapfamilyname="Source Serif 4 Display Light" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display" familyname="Source Serif 4" stylename="Display" filename="Source Serif 4-Display.ttf" postscriptfontname="SourceSerif4Roman-DisplayRegular" stylemapfamilyname="Source Serif 4 Display" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Semibold" familyname="Source Serif 4" stylename="Display Semibold" filename="Source Serif 4-Display Semibold.ttf" postscriptfontname="SourceSerif4Roman-DisplaySemibold" stylemapfamilyname="Source Serif 4 Display Semibold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Bold" familyname="Source Serif 4" stylename="Display Bold" filename="Source Serif 4-Display Bold.ttf" postscriptfontname="SourceSerif4Roman-DisplayBold" stylemapfamilyname="Source Serif 4 Display Bold" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Black" familyname="Source Serif 4" stylename="Display Black" filename="Source Serif 4-Display Black.ttf" postscriptfontname="SourceSerif4Roman-DisplayBlack" stylemapfamilyname="Source Serif 4 Display Black" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_1.0.designspace b/Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_1.0.designspace
new file mode 100644
index 00000000..e1ef12f6
--- /dev/null
+++ b/Tests/designspaceLib/data/split_output/test_v5_sourceserif_italic_1.0.designspace
@@ -0,0 +1,274 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20"/>
+ </axes>
+ <sources>
+ <source filename="../caption/master_0/SourceSerif-Italic_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_1/SourceSerif-Italic_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../caption/master_2/SourceSerif-Italic_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </source>
+ <source filename="../text/master_0/SourceSerif-Italic_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_1/SourceSerif-Italic_1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../text/master_2/SourceSerif-Italic_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </source>
+ <source filename="../display/master_0/SourceSerif-Italic_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_1/SourceSerif-Italic_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ <source filename="../display/master_2/SourceSerif-Italic_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </source>
+ </sources>
+ <variable-fonts>
+ <variable-font name="SourceSerif4Variable-Italic">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ <axis-subset name="optical"/>
+ </axis-subsets>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>f.liga</string>
+ <string>f.ligalong</string>
+ <string>tonos.cap</string>
+ <string>dieresiscmb.tight</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+ </variable-font>
+ </variable-fonts>
+ <instances>
+ <instance name="Source Serif 4 Caption ExtraLight Italic" familyname="Source Serif 4" stylename="Caption ExtraLight Italic" filename="Source Serif 4-Caption ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionExtraLight" stylemapfamilyname="Source Serif 4 Caption ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Light Italic" familyname="Source Serif 4" stylename="Caption Light Italic" filename="Source Serif 4-Caption Light Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionLight" stylemapfamilyname="Source Serif 4 Caption Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Italic" familyname="Source Serif 4" stylename="Caption Italic" filename="Source Serif 4-Caption Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionRegular" stylemapfamilyname="Source Serif 4 Caption Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Semibold Italic" familyname="Source Serif 4" stylename="Caption Semibold Italic" filename="Source Serif 4-Caption Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionSemibold" stylemapfamilyname="Source Serif 4 Caption Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Bold Italic" familyname="Source Serif 4" stylename="Caption Bold Italic" filename="Source Serif 4-Caption Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionBold" stylemapfamilyname="Source Serif 4 Caption Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Caption Black Italic" familyname="Source Serif 4" stylename="Caption Black Italic" filename="Source Serif 4-Caption Black Italic.ttf" postscriptfontname="SourceSerif4Italic-CaptionBlack" stylemapfamilyname="Source Serif 4 Caption Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText ExtraLight Italic" familyname="Source Serif 4" stylename="SmText ExtraLight Italic" filename="Source Serif 4-SmText ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextExtraLight" stylemapfamilyname="Source Serif 4 SmallText ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Light Italic" familyname="Source Serif 4" stylename="SmText Light Italic" filename="Source Serif 4-SmText Light Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextLight" stylemapfamilyname="Source Serif 4 SmallText Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Italic" familyname="Source Serif 4" stylename="SmText Italic" filename="Source Serif 4-SmText Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextRegular" stylemapfamilyname="Source Serif 4 SmallText Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Semibold Italic" familyname="Source Serif 4" stylename="SmText Semibold Italic" filename="Source Serif 4-SmText Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextSemibold" stylemapfamilyname="Source Serif 4 SmallText Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Bold Italic" familyname="Source Serif 4" stylename="SmText Bold Italic" filename="Source Serif 4-SmText Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextBold" stylemapfamilyname="Source Serif 4 SmallText Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 SmText Black Italic" familyname="Source Serif 4" stylename="SmText Black Italic" filename="Source Serif 4-SmText Black Italic.ttf" postscriptfontname="SourceSerif4Italic-SmTextBlack" stylemapfamilyname="Source Serif 4 SmallText Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 ExtraLight Italic" familyname="Source Serif 4" stylename="ExtraLight Italic" filename="Source Serif 4-ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-ExtraLight" stylemapfamilyname="Source Serif 4 ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Light Italic" familyname="Source Serif 4" stylename="Light Italic" filename="Source Serif 4-Light Italic.ttf" postscriptfontname="SourceSerif4Italic-Light" stylemapfamilyname="Source Serif 4 Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Italic" familyname="Source Serif 4" stylename="Italic" filename="Source Serif 4-Italic.ttf" postscriptfontname="SourceSerif4Italic-Regular" stylemapfamilyname="Source Serif 4 Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Semibold Italic" familyname="Source Serif 4" stylename="Semibold Italic" filename="Source Serif 4-Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-Semibold" stylemapfamilyname="Source Serif 4 Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Bold Italic" familyname="Source Serif 4" stylename="Bold Italic" filename="Source Serif 4-Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-Bold" stylemapfamilyname="Source Serif 4 Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Black Italic" familyname="Source Serif 4" stylename="Black Italic" filename="Source Serif 4-Black Italic.ttf" postscriptfontname="SourceSerif4Italic-Black" stylemapfamilyname="Source Serif 4 Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead ExtraLight Italic" familyname="Source Serif 4" stylename="Subhead ExtraLight Italic" filename="Source Serif 4-Subhead ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadExtraLight" stylemapfamilyname="Source Serif 4 Subhead ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Light Italic" familyname="Source Serif 4" stylename="Subhead Light Italic" filename="Source Serif 4-Subhead Light Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadLight" stylemapfamilyname="Source Serif 4 Subhead Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Italic" familyname="Source Serif 4" stylename="Subhead Italic" filename="Source Serif 4-Subhead Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadRegular" stylemapfamilyname="Source Serif 4 Subhead Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Semibold Italic" familyname="Source Serif 4" stylename="Subhead Semibold Italic" filename="Source Serif 4-Subhead Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadSemibold" stylemapfamilyname="Source Serif 4 Subhead Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Bold Italic" familyname="Source Serif 4" stylename="Subhead Bold Italic" filename="Source Serif 4-Subhead Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadBold" stylemapfamilyname="Source Serif 4 Subhead Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Subhead Black Italic" familyname="Source Serif 4" stylename="Subhead Black Italic" filename="Source Serif 4-Subhead Black Italic.ttf" postscriptfontname="SourceSerif4Italic-SubheadBlack" stylemapfamilyname="Source Serif 4 Subhead Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display ExtraLight Italic" familyname="Source Serif 4" stylename="Display ExtraLight Italic" filename="Source Serif 4-Display ExtraLight Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayExtraLight" stylemapfamilyname="Source Serif 4 Display ExtraLight Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Light Italic" familyname="Source Serif 4" stylename="Display Light Italic" filename="Source Serif 4-Display Light Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayLight" stylemapfamilyname="Source Serif 4 Display Light Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Italic" familyname="Source Serif 4" stylename="Display Italic" filename="Source Serif 4-Display Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayRegular" stylemapfamilyname="Source Serif 4 Display Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Semibold Italic" familyname="Source Serif 4" stylename="Display Semibold Italic" filename="Source Serif 4-Display Semibold Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplaySemibold" stylemapfamilyname="Source Serif 4 Display Semibold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Bold Italic" familyname="Source Serif 4" stylename="Display Bold Italic" filename="Source Serif 4-Display Bold Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayBold" stylemapfamilyname="Source Serif 4 Display Bold Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ <instance name="Source Serif 4 Display Black Italic" familyname="Source Serif 4" stylename="Display Black Italic" filename="Source Serif 4-Display Black Italic.ttf" postscriptfontname="SourceSerif4Italic-DisplayBlack" stylemapfamilyname="Source Serif 4 Display Black Italic" stylemapstylename="regular">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/test.designspace b/Tests/designspaceLib/data/test_v4_original.designspace
index e12f1568..d3b46c7f 100644
--- a/Tests/designspaceLib/data/test.designspace
+++ b/Tests/designspaceLib/data/test_v4_original.designspace
@@ -1,5 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<designspace format="4.1">
+ <!-- NOTE: this file is kept at format 4, to check that round-tripping it
+ doesn't upgrade the format. -->
<axes>
<axis tag="wght" name="weight" minimum="0" maximum="1000" default="0">
<labelname xml:lang="en">Wéíght</labelname>
@@ -50,6 +52,13 @@
</sources>
<instances>
<instance name="instance.ufo1" familyname="InstanceFamilyName" stylename="InstanceStyleName" filename="instances/instanceTest1.ufo" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName">
+ <stylename xml:lang="fr">Demigras</stylename>
+ <stylename xml:lang="ja">半ば</stylename>
+ <familyname xml:lang="fr">Montserrat</familyname>
+ <familyname xml:lang="ja">モンセラート</familyname>
+ <stylemapstylename xml:lang="de">Standard</stylemapstylename>
+ <stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
+ <stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
<location>
<dimension name="weight" xvalue="500"/>
<dimension name="width" xvalue="20"/>
diff --git a/Tests/designspaceLib/data/test_v5.designspace b/Tests/designspaceLib/data/test_v5.designspace
new file mode 100644
index 00000000..d2b3cdae
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5.designspace
@@ -0,0 +1,304 @@
+<?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">
+ <labelname xml:lang="en">Wéíght</labelname>
+ <labelname xml:lang="fa-IR">قطر</labelname>
+ <map input="200" output="0"/>
+ <map input="300" output="100"/>
+ <map input="400" output="368"/>
+ <map input="600" output="600"/>
+ <map input="700" output="824"/>
+ <map input="900" output="1000"/>
+ <!-- All axes provide STAT information with the "labels" element. -->
+ <labels>
+ <label uservalue="200" userminimum="200" usermaximum="250" name="Extra Light">
+ <labelname xml:lang="de">Extraleicht</labelname>
+ <labelname xml:lang="fr">Extra léger</labelname>
+ </label>
+ <label uservalue="300" userminimum="250" usermaximum="350" name="Light"/>
+ <label uservalue="400" userminimum="350" usermaximum="450" name="Regular" elidable="true"/>
+ <label uservalue="600" userminimum="450" usermaximum="650" name="Semi Bold"/>
+ <label uservalue="700" userminimum="650" usermaximum="850" name="Bold"/>
+ <label uservalue="900" userminimum="850" usermaximum="900" name="Black"/>
+ </labels>
+ </axis>
+
+ <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"/>
+ <map input="125" output="66"/>
+ <map input="150" output="990"/>
+ <labels ordering="1">
+ <label uservalue="50" name="Condensed"/>
+ <label uservalue="100" name="Normal" elidable="true" oldersibling="true"/>
+ <label uservalue="125" name="Wide"/>
+ <!-- Allow specifying only one end of the range, the other is assumed to
+ be infinity as does otlLib buildStatTable -->
+ <label uservalue="150" userminimum="150" name="Extra Wide"/>
+ </labels>
+ </axis>
+
+ <!--
+ Discrete axes provide a list of discrete values.
+ No interpolation is allowed between these.
+ -->
+ <axis tag="ital" name="Italic" values="0 1" default="0">
+ <labels>
+ <!-- Discrete axes also provide STAT information. -->
+ <label uservalue="0" name="Roman" elidable="true" linkeduservalue="1"/>
+ <label uservalue="1" name="Italic"/>
+ </labels>
+ </axis>
+ </axes>
+
+ <!-- Freestanding labels are analogues of STAT format 4 entries.
+ They give names to freestyle locations. -->
+ <labels>
+ <label name="Some Style">
+ <labelname xml:lang="fr">Un Style</labelname>
+ <location>
+ <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="Italic" uservalue="1"/>
+ </location>
+ </label>
+ </labels>
+
+ <rules processing="last">
+ <rule name="named.rule.1">
+ <conditionset>
+ <condition name="axisName_a" minimum="0" maximum="1"/>
+ <condition name="axisName_b" minimum="2" maximum="3"/>
+ </conditionset>
+ <sub name="a" with="a.alt"/>
+ </rule>
+ </rules>
+
+ <sources>
+ <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"/>
+ <features copy="1"/>
+ <info copy="1"/>
+ <glyph name="A" mute="1"/>
+ <glyph name="Z" mute="1"/>
+ <location>
+ <dimension name="Weight" xvalue="0"/>
+ <dimension name="Width" xvalue="20"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <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="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="masterTest2.ufo" name="master.ufo3" familyname="MasterFamilyName" stylename="FauxItalic">
+ <location>
+ <dimension name="Weight" xvalue="0"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+
+ <variable-fonts>
+ <!--
+ If this element is present, all output targets must be listed within it.
+ If it is not present, the full Designspace is the output, like in version 4.x.
+
+ Continuous axes can be included either:
+ * in full,
+ * or only on a reduced interval (different minimum-maximum),
+ * or only at 1 discrete location
+ Dicrete axes cannot be included in full, and we must specify a value
+ (or the compiler should assume the default value).
+ -->
+ <variable-font name="Test_WghtWdth" filename="Test_WghtWdth_different_from_name.ttf">
+ <!-- This one is the Roman (default location along ital),
+ with full range for the Weight axis. -->
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ </axis-subsets>
+ <lib>
+ <dict>
+ <key>com.vtt.source</key>
+ <string>sources/vtt/Test_WghtWdth.vtt</string>
+ </dict>
+ </lib>
+ </variable-font>
+ <variable-font name="Test_Wght">
+ <!-- This one is the Roman (default location along ital),
+ with full range for the Weight axis. -->
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ </axis-subsets>
+ <lib>
+ <dict>
+ <key>com.vtt.source</key>
+ <string>sources/vtt/Test_Wght.vtt</string>
+ </dict>
+ </lib>
+ </variable-font>
+ <variable-font name="TestCd_Wght">
+ <!-- This one is the Roman (default location along ital),
+ with full range for the Weight axis. -->
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width" uservalue="0"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="TestWd_Wght">
+ <!-- This one is the Roman (default location along ital),
+ with full range for the Weight axis. -->
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width" uservalue="1000"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="TestItalic_Wght">
+ <!-- This one is the Italic, with full range for the Weight axis. -->
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Italic" uservalue="1"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="TestRB_Wght">
+ <!-- As an example, this would be the Roman with a reduced range. -->
+ <axis-subsets>
+ <axis-subset name="Weight" userminimum="400" usermaximum="700" userdefault="400"/>
+ <axis-subset name="Italic" uservalue="0"/>
+ </axis-subsets>
+ </variable-font>
+ </variable-fonts>
+
+ <instances>
+ <instance name="instance.ufo1" familyname="InstanceFamilyName" stylename="InstanceStyleName" filename="instances/instanceTest1.ufo" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName">
+ <stylename xml:lang="fr">Demigras</stylename>
+ <stylename xml:lang="ja">半ば</stylename>
+ <familyname xml:lang="fr">Montserrat</familyname>
+ <familyname xml:lang="ja">モンセラート</familyname>
+ <stylemapstylename xml:lang="de">Standard</stylemapstylename>
+ <stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
+ <stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
+ <location>
+ <dimension name="Weight" xvalue="500"/>
+ <dimension name="Width" xvalue="20"/>
+ </location>
+
+ <!-- The following elements are deprecated in v5.0. They can still be
+ read, but they won't be written out again (they don't roundtrip). -->
+ <!-- ROUNDTRIP_TEST_REMOVE_ME_BEGIN -->
+ <glyphs>
+ <glyph mute="1" unicode="0x123 0x124 0x125" name="arrow"/>
+ </glyphs>
+ <kerning/>
+ <info/>
+ <!-- ROUNDTRIP_TEST_REMOVE_ME_END -->
+
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.binaryData</key>
+ <data>
+ PGJpbmFyeSBndW5rPg==
+ </data>
+ <key>com.coolDesignspaceApp.specimenText</key>
+ <string>Hamburgerwhatever</string>
+ </dict>
+ </lib>
+ </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"/>
+ </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"/>
+ </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"/>
+ </location>
+ </master>
+ <master glyphname="CC" source="master.ufo2">
+ <location>
+ <dimension name="Weight" xvalue="900"/>
+ <dimension name="Width" xvalue="900"/>
+ </location>
+ </master>
+ </masters>
+ </glyph>
+ <glyph name="arrow2"/>
+ </glyphs>
+ <kerning/>
+ <info/>
+ <!-- ROUNDTRIP_TEST_REMOVE_ME_END -->
+ </instance>
+
+ <!--
+ These instances will derive all their data from the data above.
+
+ Instances can specify their location either:
+ - using the name of a location label
+ - with design coordinates (xvalue="")
+ - with user coordinates (uservalue="")
+ - with a mix of both coordinate systems
+ -->
+ <instance location="Some Style"/>
+ <instance>
+ <location>
+ <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="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="Weight" uservalue="300"/>
+ <dimension name="Width" uservalue="130"/>
+ <dimension name="Italic" uservalue="1"/>
+ </location>
+ </instance>
+ </instances>
+
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.previewSize</key>
+ <integer>30</integer>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5_MutatorSans_and_Serif.designspace b/Tests/designspaceLib/data/test_v5_MutatorSans_and_Serif.designspace
new file mode 100644
index 00000000..d0fbb3d2
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5_MutatorSans_and_Serif.designspace
@@ -0,0 +1,206 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes elidedfallbackname="Regular">
+ <axis tag="SRIF" name="serif" values="0 1" default="0">
+ <labels>
+ <label uservalue="0" name="Sans"/>
+ <label uservalue="1" name="Serif"/>
+ </labels>
+ </axis>
+ <axis tag="wght" name="weight" minimum="300" maximum="700" default="300">
+ <map input="300" output="0"/>
+ <map input="500" output="500"/>
+ <map input="700" output="1000"/>
+ <labels>
+ <label uservalue="300" userminimum="300" usermaximum="400" name="Light"/>
+ <label uservalue="500" userminimum="400" usermaximum="600" name="Medium"/>
+ <label uservalue="700" userminimum="600" usermaximum="700" name="Bold"/>
+ </labels>
+ </axis>
+ <axis tag="wdth" name="width" minimum="50" maximum="200" default="50">
+ <map input="50" output="0"/>
+ <map input="100" output="500"/>
+ <map input="200" output="1000"/>
+ <labels>
+ <label uservalue="50" userminimum="50" usermaximum="75" name="Condensed"/>
+ <label uservalue="100" userminimum="75" usermaximum="125" name="Normal" elidable="true"/>
+ <label uservalue="200" userminimum="125" usermaximum="200" name="Extended"/>
+ </labels>
+ </axis>
+ </axes>
+ <labels>
+ <label name="S1">
+ <location>
+ <dimension name="width" uservalue="158.9044"/>
+ <dimension name="weight" uservalue="610.2436"/>
+ </location>
+ </label>
+ <label name="S2">
+ <location>
+ <dimension name="width" uservalue="159.1956"/>
+ <dimension name="weight" uservalue="642.2196"/>
+ </location>
+ </label>
+ </labels>
+ <rules>
+ <rule name="fold_I_serifs">
+ <conditionset>
+ <condition name="width" minimum="0" maximum="328"/>
+ <condition name="serif" minimum="0" maximum="0"/>
+ </conditionset>
+ <sub name="I" with="I.narrow"/>
+ </rule>
+ <rule name="fold_S_terminals">
+ <conditionset>
+ <condition name="width" minimum="0" maximum="1000"/>
+ <condition name="weight" minimum="0" maximum="500"/>
+ <condition name="serif" minimum="0" maximum="0.5"/>
+ </conditionset>
+ <sub name="S" with="S.closed"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="MutatorSansLightCondensed.ufo" name="master.MutatorMathTest.LightCondensed.0" familyname="MutatorMathTest" stylename="LightCondensed">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="MutatorSansBoldCondensed.ufo" name="master.MutatorMathTest.BoldCondensed.1" familyname="MutatorMathTest" stylename="BoldCondensed">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="MutatorSansLightWide.ufo" name="master.MutatorMathTest.LightWide.2" familyname="MutatorMathTest" stylename="LightWide">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="MutatorSansBoldWide.ufo" name="master.MutatorMathTest.BoldWide.3" familyname="MutatorMathTest" stylename="BoldWide">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="MutatorSansLightCondensed.ufo" name="support.crossbar" layer="support.crossbar">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="weight" xvalue="700"/>
+ </location>
+ </source>
+ <source filename="MutatorSansLightCondensed.ufo" name="support.S.wide" layer="support.S.wide">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="700"/>
+ </location>
+ </source>
+ <source filename="MutatorSansLightCondensed.ufo" name="support.S.middle" layer="support.S.middle">
+ <location>
+ <dimension name="width" xvalue="569.078000"/>
+ <dimension name="weight" xvalue="700"/>
+ </location>
+ </source>
+ <source filename="MutatorSerifLightCondensed.ufo" familyname="MutatorMathTest" stylename="SerifLightCondensed">
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="serif" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="MutatorSerifLightWide.ufo" familyname="MutatorMathTest" stylename="SerifLightWide">
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="serif" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+ <variable-fonts>
+ <variable-font name="MutatorSansVariable_Weight_Width">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ <axis-subset name="width"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="MutatorSansVariable_Weight">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="MutatorSansVariable_Width">
+ <axis-subsets>
+ <axis-subset name="width"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="MutatorSerifVariable_Width">
+ <axis-subsets>
+ <axis-subset name="serif" uservalue="1"/>
+ <axis-subset name="width"/>
+ </axis-subsets>
+ </variable-font>
+ </variable-fonts>
+ <instances>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="327"/>
+ <dimension name="weight" xvalue="500"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="327"/>
+ <dimension name="weight" xvalue="500"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="569.078"/>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="1000"/>
+ <dimension name="weight" xvalue="500"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="serif" xvalue="0"/>
+ </location>
+ </instance>
+ <instance>
+ <location>
+ <dimension name="width" xvalue="0"/>
+ <dimension name="serif" xvalue="1"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5_aktiv.designspace b/Tests/designspaceLib/data/test_v5_aktiv.designspace
new file mode 100644
index 00000000..32dfd1ae
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5_aktiv.designspace
@@ -0,0 +1,621 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
+ <map input="100" output="22"/>
+ <map input="200" output="38"/>
+ <map input="300" output="57"/>
+ <map input="400" output="84"/>
+ <map input="500" output="98"/>
+ <map input="600" output="115"/>
+ <map input="700" output="133"/>
+ <map input="800" output="158"/>
+ <map input="900" output="185"/>
+ <labels ordering="1">
+ <label uservalue="100" name="Hair"/>
+ <label uservalue="200" name="Thin"/>
+ <label uservalue="300" name="Light"/>
+ <label uservalue="400" name="Regular" elidable="true" linkeduservalue="700"/>
+ <label uservalue="500" name="Medium"/>
+ <label uservalue="600" name="SemiBold"/>
+ <label uservalue="700" name="Bold"/>
+ <label uservalue="800" name="XBold"/>
+ <label uservalue="900" name="Black"/>
+ </labels>
+ </axis>
+ <axis tag="wdth" name="Width" minimum="75" maximum="125" default="100">
+ <labels ordering="0">
+ <label uservalue="75" name="Cd"/>
+ <label uservalue="100" name="Normal" elidable="true"/>
+ <label uservalue="125" name="Ex"/>
+ </labels>
+ </axis>
+ <axis tag="ital" name="Italic" minimum="0" maximum="1" default="0">
+ <labels ordering="2">
+ <label uservalue="0" name="Upright" elidable="true" linkeduservalue="1"/>
+ <label uservalue="1" name="Italic"/>
+ </labels>
+ </axis>
+ </axes>
+
+ <rules processing="last">
+ <rule name="BRACKET.CYR">
+ <conditionset>
+ <condition name="Italic" minimum="0.1" maximum="1"/>
+ </conditionset>
+ <sub name="ghe.loclSRB" with="ghe.ital.loclSRB"/>
+ <sub name="ghe.loclMKD" with="ghe.ital.loclMKD"/>
+ <sub name="de.loclMKDSRB" with="de.ital.loclMKDSRB"/>
+ <sub name="pe.loclMKDSRB" with="pe.ital.loclMKDSRB"/>
+ <sub name="te.loclMKDSRB" with="te.ital.loclMKDSRB"/>
+ <sub name="gje.loclMKD" with="gje.ital.loclMKD"/>
+ <sub name="sha.loclMKDSRB" with="sha.ital.loclMKDSRB"/>
+ </rule>
+ <rule name="BRACKET.116.185">
+ <conditionset>
+ <condition name="Weight" minimum="116" maximum="185"/>
+ <condition name="Width" minimum="75" maximum="97.5"/>
+ </conditionset>
+ <sub name="cent" with="cent.BRACKET.130"/>
+ <sub name="dollar" with="dollar.BRACKET.130"/>
+ </rule>
+ </rules>
+
+ <sources>
+ <source filename="AktivGroteskCd_Hair.ufo" name="Aktiv Grotesk Cd Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskCd_HairIt.ufo" name="Aktiv Grotesk Cd Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_Hair.ufo" name="Aktiv Grotesk Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_HairIt.ufo" name="Aktiv Grotesk Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskEx_Hair.ufo" name="Aktiv Grotesk Ex Hair">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskEx_HairIt.ufo" name="Aktiv Grotesk Ex Hair Italic">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskCd_Rg.ufo" name="Aktiv Grotesk Cd">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskCd_It.ufo" name="Aktiv Grotesk Cd Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" familyname="Aktiv Grotesk">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_Rg.ufo" name="Aktiv Grotesk Regular" layer="{126,100,0}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_It.ufo" name="Aktiv Grotesk Italic {126,100,1}" layer="{126,100,1}">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskEx_Rg.ufo" name="Aktiv Grotesk Ex">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskEx_It.ufo" name="Aktiv Grotesk Ex Italic">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskCd_Blk.ufo" name="Aktiv Grotesk Cd Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskCd_BlkIt.ufo" name="Aktiv Grotesk Cd Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_Blk.ufo" name="Aktiv Grotesk Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGrotesk_BlkIt.ufo" name="Aktiv Grotesk Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskEx_Blk.ufo" name="Aktiv Grotesk Ex Black">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="AktivGroteskEx_BlkIt.ufo" name="Aktiv Grotesk Ex Black Italic">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+
+ <variable-fonts>
+ <variable-font name="AktivGroteskVF_WghtWdthItal">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ <axis-subset name="Italic"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_WghtWdth">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_Wght">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_Italics_WghtWdth">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Width"/>
+ <axis-subset name="Italic" uservalue="1"/>
+ </axis-subsets>
+ </variable-font>
+ <variable-font name="AktivGroteskVF_Italics_Wght">
+ <axis-subsets>
+ <axis-subset name="Weight"/>
+ <axis-subset name="Italic" uservalue="1"/>
+ </axis-subsets>
+ </variable-font>
+ </variable-fonts>
+
+ <instances>
+ <instance filename="instances/AktivGroteskCd_Hair.ufo">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_HairIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Hair.ufo">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_HairIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Hair.ufo">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_HairIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="22"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_Th.ufo">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_ThIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Th.ufo">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_ThIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Th.ufo">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_ThIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="38"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_Lt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_LtIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Lt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_LtIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Lt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_LtIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="57"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_Rg.ufo">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_It.ufo">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Rg.ufo">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_It.ufo">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Rg.ufo">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_It.ufo">
+ <location>
+ <dimension name="Weight" xvalue="84"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_Md.ufo">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_MdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Md.ufo">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_MdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Md.ufo">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_MdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="98"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="../build/instances/AktivGroteskCd_SBd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="../build/instances/AktivGroteskCd_SBdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="../build/instances/AktivGrotesk_SBd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="../build/instances/AktivGrotesk_SBdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="../build/instances/AktivGroteskEx_SBd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="../build/instances/AktivGroteskEx_SBdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="115"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_Bd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_BdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Bd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_BdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Bd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_BdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="133"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_XBd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_XBdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_XBd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_XBdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_XBd.ufo">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_XBdIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="158"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_Blk.ufo">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskCd_BlkIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_Blk.ufo">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGrotesk_BlkIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="100"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_Blk.ufo">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance filename="instances/AktivGroteskEx_BlkIt.ufo">
+ <location>
+ <dimension name="Weight" xvalue="185"/>
+ <dimension name="Width" xvalue="125"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5_decovar.designspace b/Tests/designspaceLib/data/test_v5_decovar.designspace
new file mode 100644
index 00000000..fd31626e
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5_decovar.designspace
@@ -0,0 +1,242 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="BLDA" name="Inline" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMD" name="Shearded" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMC" name="Rounded Slab" minimum="0" maximum="1000" default="0"/>
+ <axis tag="SKLD" name="Stripes" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRML" name="Worm Terminal" minimum="0" maximum="1000" default="0"/>
+ <axis tag="SKLA" name="Inline Skeleton" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMF" name="Open Inline Terminal" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMK" name="Inline Terminal" minimum="0" maximum="1000" default="0"/>
+ <axis tag="BLDB" name="Worm" minimum="0" maximum="1000" default="0"/>
+ <axis tag="WMX2" name="Weight" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMB" name="Flared" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMA" name="Rounded" minimum="0" maximum="1000" default="0"/>
+ <axis tag="SKLB" name="Worm Skeleton" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRMG" name="Slab" minimum="0" maximum="1000" default="0"/>
+ <axis tag="TRME" name="Bifurcated" minimum="0" maximum="1000" default="0"/>
+ </axes>
+
+ <!-- Labels without location or just partly specified location are at the default location plus the explicitly given sub-locations. -->
+ <labels>
+ <label name="Default" elidable="true"/>
+ <label name="Open">
+ <labelname xml:lang="de">Offen</labelname>
+ <location>
+ <dimension name="Inline" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Worm">
+ <location>
+ <dimension name="Worm" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Checkered">
+ <location>
+ <dimension name="Inline Skeleton" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Checkered Reverse">
+ <location>
+ <dimension name="Inline Terminal" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Striped">
+ <location>
+ <dimension name="Stripes" uservalue="500"/>
+ </location>
+ </label>
+ <label name="Rounded">
+ <location>
+ <dimension name="Rounded" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Flared">
+ <location>
+ <dimension name="Flared" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Flared Open">
+ <location>
+ <dimension name="Inline Skeleton" uservalue="1000"/>
+ <dimension name="Flared" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Rounded Slab">
+ <location>
+ <dimension name="Rounded Slab" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Sheared">
+ <location>
+ <dimension name="Shearded" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Bifurcated">
+ <location>
+ <dimension name="Bifurcated" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Inline">
+ <location>
+ <dimension name="Inline Skeleton" uservalue="500"/>
+ <dimension name="Open Inline Terminal" uservalue="500"/>
+ </location>
+ </label>
+ <label name="Slab">
+ <location>
+ <dimension name="Slab" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Contrast">
+ <location>
+ <dimension name="Weight" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Fancy">
+ <location>
+ <dimension name="Inline Skeleton" uservalue="1000"/>
+ <dimension name="Weight" uservalue="1000"/>
+ <dimension name="Flared" uservalue="1000"/>
+ </location>
+ </label>
+ <label name="Mayhem">
+ <location>
+ <dimension name="Rounded Slab" uservalue="750"/>
+ <dimension name="Worm Terminal" uservalue="250"/>
+ <dimension name="Inline Skeleton" uservalue="1000"/>
+ <dimension name="Open Inline Terminal" uservalue="250"/>
+ <dimension name="Inline Terminal" uservalue="250"/>
+ <dimension name="Worm" uservalue="1000"/>
+ <dimension name="Weight" uservalue="750"/>
+ <dimension name="Flared" uservalue="500"/>
+ <dimension name="Rounded" uservalue="500"/>
+ <dimension name="Worm Skeleton" uservalue="1000"/>
+ <dimension name="Slab" uservalue="750"/>
+ <dimension name="Bifurcated" uservalue="500"/>
+ </location>
+ </label>
+ </labels>
+
+ <sources>
+ <source filename="DecovarAlpha-Regular24.ufo" name="master_Regular"/>
+ <source filename="DecovarAlpha-Regular24SkelA.ufo" name="master_sklA">
+ <location>
+ <dimension name="Inline Skeleton" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24SkelD2.ufo" name="master_sklD2">
+ <location>
+ <dimension name="Stripes" xvalue="500"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24SkelD4.ufo" name="master_sklD4">
+ <location>
+ <dimension name="Stripes" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24SkelB2.ufo" name="master_sklB2">
+ <location>
+ <dimension name="Worm Skeleton" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermA.ufo" name="master_trmA">
+ <location>
+ <dimension name="Rounded" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermB.ufo" name="master_trmB">
+ <location>
+ <dimension name="Flared" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermC.ufo" name="master_trmC">
+ <location>
+ <dimension name="Rounded Slab" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermD.ufo" name="master_trmD">
+ <location>
+ <dimension name="Shearded" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermE.ufo" name="master_trmE">
+ <location>
+ <dimension name="Bifurcated" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermF.ufo" name="master_trmF">
+ <location>
+ <dimension name="Open Inline Terminal" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermG.ufo" name="master_trmG">
+ <location>
+ <dimension name="Slab" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermSkelA.ufo" name="master_trmK">
+ <location>
+ <dimension name="Inline Terminal" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24TermSkelB.ufo" name="master_trmL">
+ <location>
+ <dimension name="Worm Terminal" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24WeightMax1.ufo" name="master_wmx1">
+ <location>
+ <dimension name="Weight" xvalue="5"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24WeightMax1.5.ufo" name="master_wmx1.5">
+ <location>
+ <dimension name="Weight" xvalue="507.806"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24WeightMax2.ufo" name="master_wmx2">
+ <location>
+ <dimension name="Weight" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24bldA.ufo" name="master_bldA">
+ <location>
+ <dimension name="Inline" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="DecovarAlpha-Regular24bldB.ufo" name="master_bldB">
+ <location>
+ <dimension name="Worm" xvalue="1000"/>
+ </location>
+ </source>
+ </sources>
+
+ <instances>
+ <!--
+ This designspace defines LocationLabels (STAT format 4) for various
+ points within the design space. In order to define instances at these
+ locations, DS version 5 provides the `location="..."` attribute which
+ means that the instance is in the same location as the LocationLabel
+ with the given name.
+ -->
+ <instance location="Default" filename="DecovarAlpha-Default.ufo"/>
+ <instance location="Open" filename="DecovarAlpha-Open.ufo"/>
+ <instance location="Worm" filename="DecovarAlpha-Worm.ufo"/>
+ <instance location="Checkered" filename="DecovarAlpha-Checkered.ufo"/>
+ <instance location="Checkered Reverse" filename="DecovarAlpha-CheckeredReverse.ufo"/>
+ <instance location="Striped" filename="DecovarAlpha-Striped.ufo"/>
+ <instance location="Rounded" filename="DecovarAlpha-Rounded.ufo"/>
+ <instance location="Flared" filename="DecovarAlpha-Flared.ufo"/>
+ <instance location="Flared Open" filename="DecovarAlpha-FlaredOpen.ufo"/>
+ <instance location="Rounded Slab" filename="DecovarAlpha-RoundedSlab.ufo"/>
+ <instance location="Sheared" filename="DecovarAlpha-Sheared.ufo"/>
+ <instance location="Bifurcated" filename="DecovarAlpha-Bifurcated.ufo"/>
+ <instance location="Inline" filename="DecovarAlpha-Inline.ufo"/>
+ <instance location="Slab" filename="DecovarAlpha-Slab.ufo"/>
+ <instance location="Contrast" filename="DecovarAlpha-Contrast.ufo"/>
+ <instance location="Fancy" filename="DecovarAlpha-Fancy.ufo"/>
+ <instance location="Mayhem" filename="DecovarAlpha-Mayhem.ufo"/>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5_discrete.designspace b/Tests/designspaceLib/data/test_v5_discrete.designspace
new file mode 100644
index 00000000..f42f2dc9
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5_discrete.designspace
@@ -0,0 +1,139 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" values="400 700 900" default="400">
+ <labels>
+ <label uservalue="400" name="Regular" elidable="true" linkeduservalue="700"/>
+ <label uservalue="700" name="Bold"/>
+ <label uservalue="900" name="Black"/>
+ </labels>
+ </axis>
+ <axis tag="wdth" name="Width" values="75 100" default="100">
+ <labels>
+ <label uservalue="75" name="Narrow"/>
+ <label uservalue="100" name="Normal" elidable="true"/>
+ </labels>
+ </axis>
+ <axis tag="ital" name="Italic" values="0 1" default="0">
+ <labels>
+ <label uservalue="0" name="Roman" elidable="true" linkeduservalue="1"/>
+ <label uservalue="1" name="Italic"/>
+ </labels>
+ </axis>
+ </axes>
+
+ <sources>
+ <source filename="arial.ufo" name="Arial Regular">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ </location>
+ </source>
+ <source filename="arialbd.ufo" name="Arial Bold">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ </location>
+ </source>
+ <source filename="ariblk.ufo" name="Arial Black">
+ <location>
+ <dimension name="Weight" xvalue="900"/>
+ </location>
+ </source>
+ <source filename="ariali.ufo" name="Arial Italic">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="arialbi.ufo" name="Arial Bold Italic">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="ARIALN.ufo" name="Arial Narrow">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="ARIALNB.ufo" name="Arial Narrow Bold">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </source>
+ <source filename="ARIALNBI.ufo" name="Arial Narrow Bold Italic">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="ARIALNI.ufo" name="Arial Narrow Italic">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+
+ <instances>
+ <instance name="Arial Regular">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ </location>
+ </instance>
+ <instance name="Arial Bold">
+ <!-- Should be stylemapstylename="bold" -->
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ </location>
+ </instance>
+ <instance name="Arial Black">
+ <location>
+ <dimension name="Weight" xvalue="900"/>
+ </location>
+ </instance>
+ <instance name="Arial Italic">
+ <!-- Should be stylemapstylename="italic" -->
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Arial Bold Italic">
+ <!-- Should be stylemapstylename="bold italic" -->
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Arial Narrow">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Arial Narrow Bold">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ <dimension name="Width" xvalue="75"/>
+ </location>
+ </instance>
+ <instance name="Arial Narrow Bold Italic">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance name="Arial Narrow Italic">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ <dimension name="Width" xvalue="75"/>
+ <dimension name="Italic" xvalue="1"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5_original.designspace b/Tests/designspaceLib/data/test_v5_original.designspace
new file mode 100644
index 00000000..d144a073
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5_original.designspace
@@ -0,0 +1,90 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <!--
+ NOTE: this file is the same as test_v4_original, except:
+ - the format is 5,
+ - source location width = 20 are not written out because it's the default
+ - instance <glyphs>, <kerning>, <info> are removed because deprecated
+ -->
+ <axes>
+ <axis tag="wght" name="weight" minimum="0" maximum="1000" default="0">
+ <labelname xml:lang="en">Wéíght</labelname>
+ <labelname xml:lang="fa-IR">قطر</labelname>
+ </axis>
+ <axis tag="wdth" name="width" minimum="0" maximum="1000" default="15" hidden="1">
+ <labelname xml:lang="fr">Chasse</labelname>
+ <map input="0" output="10"/>
+ <map input="15" output="20"/>
+ <map input="401" output="66"/>
+ <map input="1000" output="990"/>
+ </axis>
+ </axes>
+ <rules processing="last">
+ <rule name="named.rule.1">
+ <conditionset>
+ <condition name="axisName_a" minimum="0" maximum="1"/>
+ <condition name="axisName_b" minimum="2" maximum="3"/>
+ </conditionset>
+ <sub name="a" with="a.alt"/>
+ </rule>
+ </rules>
+ <sources>
+ <source filename="masters/masterTest1.ufo" name="master.ufo1" familyname="MasterFamilyName" stylename="MasterStyleNameOne">
+ <lib copy="1"/>
+ <features copy="1"/>
+ <info copy="1"/>
+ <glyph name="A" mute="1"/>
+ <glyph name="Z" mute="1"/>
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="masters/masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="MasterStyleNameTwo">
+ <kerning mute="1"/>
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </source>
+ <source filename="masters/masterTest2.ufo" name="master.ufo2" familyname="MasterFamilyName" stylename="Supports" layer="supports">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ </location>
+ </source>
+ </sources>
+ <instances>
+ <instance name="instance.ufo1" familyname="InstanceFamilyName" stylename="InstanceStyleName" filename="instances/instanceTest1.ufo" postscriptfontname="InstancePostscriptName" stylemapfamilyname="InstanceStyleMapFamilyName" stylemapstylename="InstanceStyleMapStyleName">
+ <stylename xml:lang="fr">Demigras</stylename>
+ <stylename xml:lang="ja">半ば</stylename>
+ <familyname xml:lang="fr">Montserrat</familyname>
+ <familyname xml:lang="ja">モンセラート</familyname>
+ <stylemapstylename xml:lang="de">Standard</stylemapstylename>
+ <stylemapfamilyname xml:lang="de">Montserrat Halbfett</stylemapfamilyname>
+ <stylemapfamilyname xml:lang="ja">モンセラート SemiBold</stylemapfamilyname>
+ <location>
+ <dimension name="weight" xvalue="500"/>
+ </location>
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.binaryData</key>
+ <data>
+ PGJpbmFyeSBndW5rPg==
+ </data>
+ <key>com.coolDesignspaceApp.specimenText</key>
+ <string>Hamburgerwhatever</string>
+ </dict>
+ </lib>
+ </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"/>
+ </location>
+ </instance>
+ </instances>
+ <lib>
+ <dict>
+ <key>com.coolDesignspaceApp.previewSize</key>
+ <integer>30</integer>
+ </dict>
+ </lib>
+</designspace>
diff --git a/Tests/designspaceLib/data/test_v5_sourceserif.designspace b/Tests/designspaceLib/data/test_v5_sourceserif.designspace
new file mode 100644
index 00000000..d3a32177
--- /dev/null
+++ b/Tests/designspaceLib/data/test_v5_sourceserif.designspace
@@ -0,0 +1,646 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="weight" minimum="200" maximum="900" default="400">
+ <map input="200" output="0"/>
+ <map input="300" output="145"/>
+ <map input="400" output="394"/>
+ <map input="600" output="594"/>
+ <map input="700" output="823"/>
+ <map input="900" output="1000"/>
+ <labels ordering="1">
+ <label uservalue="200" userminimum="200" usermaximum="250" name="ExtraLight"/>
+ <label uservalue="300" userminimum="250" usermaximum="350" name="Light"/>
+ <label uservalue="400" userminimum="350" usermaximum="450" name="Regular" elidable="true"/>
+ <label uservalue="600" userminimum="550" usermaximum="650" name="Semibold"/>
+ <label uservalue="700" userminimum="650" usermaximum="750" name="Bold"/>
+ <label uservalue="775" userminimum="750" usermaximum="800" name="ExtraBold"/>
+ <label uservalue="900" userminimum="800" usermaximum="900" name="Black"/>
+ </labels>
+ </axis>
+ <axis tag="opsz" name="optical" minimum="8" maximum="60" default="20">
+ <labels ordering="0">
+ <label uservalue="8" userminimum="8" usermaximum="12" name="Caption"/>
+ <label uservalue="16" userminimum="12" usermaximum="18" name="SmallText"/>
+ <label uservalue="20" userminimum="18" usermaximum="26" name="Text" elidable="true"/>
+ <label uservalue="32" userminimum="26" usermaximum="48" name="Subhead"/>
+ <label uservalue="60" userminimum="48" usermaximum="60" name="Display"/>
+ </labels>
+ </axis>
+ <axis tag="ital" name="italic" values="0 1" default="0">
+ <labels ordering="2">
+ <label uservalue="0" name="Roman" elidable="true"/>
+ <label uservalue="1" name="Italic"/>
+ </labels>
+ </axis>
+ </axes>
+
+ <sources>
+ <source filename="caption/master_0/SourceSerif_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="caption/master_1/SourceSerif_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="caption/master_2/SourceSerif_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="text/master_0/SourceSerif_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="text/master_1/SourceSerif_1.ufo" familyname="Source Serif 4">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="text/master_2/SourceSerif_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="display/master_0/SourceSerif_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="display/master_1/SourceSerif_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+ <source filename="display/master_2/SourceSerif_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </source>
+
+ <source filename="caption/master_0/SourceSerif-Italic_c0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="caption/master_1/SourceSerif-Italic_c1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="caption/master_2/SourceSerif-Italic_c2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="text/master_0/SourceSerif-Italic_0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="text/master_1/SourceSerif-Italic_1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="text/master_2/SourceSerif-Italic_2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="display/master_0/SourceSerif-Italic_d0.ufo">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="display/master_1/SourceSerif-Italic_d1.ufo">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ <source filename="display/master_2/SourceSerif-Italic_d2.ufo">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </source>
+ </sources>
+
+ <variable-fonts>
+ <variable-font name="SourceSerif4Variable-Roman">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ <axis-subset name="optical"/>
+ <axis-subset name="italic" uservalue="0"/>
+ </axis-subsets>
+ <!-- The per-variable-font lib is to be merged into the global lib on
+ conversion to a DS v4, overwriting keys in the global lib. -->
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>tonos.cap</string>
+ <string>f.ligalong</string>
+ <string>dieresiscmb.tight</string>
+ <string>IJ</string>
+ <string>Tbar</string>
+ <string>colontriangularmod</string>
+ <string>crossmark</string>
+ <string>ij</string>
+ <string>overline</string>
+ <string>similar</string>
+ <string>tbar</string>
+ <string>triangularbullet</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+ </variable-font>
+ <variable-font name="SourceSerif4Variable-Italic">
+ <axis-subsets>
+ <axis-subset name="weight"/>
+ <axis-subset name="optical"/>
+ <axis-subset name="italic" uservalue="1"/>
+ </axis-subsets>
+ <lib>
+ <dict>
+ <key>public.skipExportGlyphs</key>
+ <array>
+ <string>caron.alt</string>
+ <string>commabelowcmb.alt</string>
+ <string>f.liga</string>
+ <string>f.ligalong</string>
+ <string>tonos.cap</string>
+ <string>dieresiscmb.tight</string>
+ <string>turkicdsccmb</string>
+ </array>
+ </dict>
+ </lib>
+ </variable-font>
+ </variable-fonts>
+
+ <instances>
+ <instance postscriptfontname="SourceSerif4Roman-CaptionExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-CaptionLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-CaptionRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-CaptionSemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-CaptionBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-CaptionBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="SmText ExtraLight" postscriptfontname="SourceSerif4Roman-SmTextExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Light" postscriptfontname="SourceSerif4Roman-SmTextLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="SmText" postscriptfontname="SourceSerif4Roman-SmTextRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Semibold" postscriptfontname="SourceSerif4Roman-SmTextSemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Bold" postscriptfontname="SourceSerif4Roman-SmTextBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Black" postscriptfontname="SourceSerif4Roman-SmTextBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-ExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-Light">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance stylename="Regular" postscriptfontname="SourceSerif4Roman-Regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-Semibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-Bold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-Black">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-SubheadExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-SubheadLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-SubheadRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-SubheadSemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-SubheadBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-SubheadBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-DisplayExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-DisplayLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-DisplayRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-DisplaySemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-DisplayBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Roman-DisplayBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="0"/>
+ </location>
+ </instance>
+
+ <instance postscriptfontname="SourceSerif4Italic-CaptionExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-CaptionLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-CaptionRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-CaptionSemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-CaptionBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-CaptionBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="8"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance stylename="SmText ExtraLight Italic" postscriptfontname="SourceSerif4Italic-SmTextExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Light Italic" postscriptfontname="SourceSerif4Italic-SmTextLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Italic" postscriptfontname="SourceSerif4Italic-SmTextRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Semibold Italic" postscriptfontname="SourceSerif4Italic-SmTextSemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Bold Italic" postscriptfontname="SourceSerif4Italic-SmTextBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance stylename="SmText Black Italic" postscriptfontname="SourceSerif4Italic-SmTextBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="16"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-ExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-Light">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-Regular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-Semibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-Bold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-Black">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="20"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-SubheadExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-SubheadLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-SubheadRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-SubheadSemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-SubheadBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-SubheadBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="32"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-DisplayExtraLight">
+ <location>
+ <dimension name="weight" xvalue="0"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-DisplayLight">
+ <location>
+ <dimension name="weight" xvalue="145"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-DisplayRegular">
+ <location>
+ <dimension name="weight" xvalue="394"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-DisplaySemibold">
+ <location>
+ <dimension name="weight" xvalue="594"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-DisplayBold">
+ <location>
+ <dimension name="weight" xvalue="823"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ <instance postscriptfontname="SourceSerif4Italic-DisplayBlack">
+ <location>
+ <dimension name="weight" xvalue="1000"/>
+ <dimension name="optical" xvalue="60"/>
+ <dimension name="italic" xvalue="1"/>
+ </location>
+ </instance>
+ </instances>
+</designspace>
diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py
index b6ee9d65..ee2d19e6 100644
--- a/Tests/designspaceLib/designspace_test.py
+++ b/Tests/designspaceLib/designspace_test.py
@@ -1,15 +1,26 @@
# coding=utf-8
import os
-import sys
-import pytest
-import warnings
+import re
-from fontTools.misc import plistlib
-from fontTools.designspaceLib import (
- DesignSpaceDocument, SourceDescriptor, AxisDescriptor, RuleDescriptor,
- InstanceDescriptor, evaluateRule, processRules, posix, DesignSpaceDocumentError)
+import pytest
from fontTools import ttLib
+from fontTools.designspaceLib import (
+ AxisDescriptor,
+ AxisLabelDescriptor,
+ DesignSpaceDocument,
+ DesignSpaceDocumentError,
+ DiscreteAxisDescriptor,
+ InstanceDescriptor,
+ RuleDescriptor,
+ SourceDescriptor,
+ evaluateRule,
+ posix,
+ processRules,
+)
+from fontTools.designspaceLib.types import Range
+from fontTools.misc import plistlib
+
def _axesAsDict(axes):
"""
@@ -30,19 +41,22 @@ def _axesAsDict(axes):
def assert_equals_test_file(path, test_filename):
- with open(path) as fp:
+ with open(path, encoding="utf-8") as fp:
actual = fp.read()
test_path = os.path.join(os.path.dirname(__file__), test_filename)
- with open(test_path) as fp:
+ with open(test_path, encoding="utf-8") as fp:
expected = fp.read()
+ expected = re.sub(r"<!--(.|\n)*?-->", "", expected)
+ expected = re.sub(r"\s*\n+", "\n", expected)
assert actual == expected
def test_fill_document(tmpdir):
tmpdir = str(tmpdir)
- testDocPath = os.path.join(tmpdir, "test.designspace")
+ testDocPath = os.path.join(tmpdir, "test_v4.designspace")
+ testDocPath5 = os.path.join(tmpdir, "test_v5.designspace")
masterPath1 = os.path.join(tmpdir, "masters", "masterTest1.ufo")
masterPath2 = os.path.join(tmpdir, "masters", "masterTest2.ufo")
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
@@ -121,6 +135,10 @@ def test_fill_document(tmpdir):
i1.postScriptFontName = "InstancePostscriptName"
i1.styleMapFamilyName = "InstanceStyleMapFamilyName"
i1.styleMapStyleName = "InstanceStyleMapStyleName"
+ i1.localisedStyleName = dict(fr="Demigras", ja="半ば")
+ i1.localisedFamilyName = dict(fr="Montserrat", ja="モンセラート")
+ i1.localisedStyleMapStyleName = dict(de="Standard")
+ i1.localisedStyleMapFamilyName = dict(de="Montserrat Halbfett", ja="モンセラート SemiBold")
glyphData = dict(name="arrow", mute=True, unicodes=[0x123, 0x124, 0x125])
i1.glyphs['arrow'] = glyphData
i1.lib['com.coolDesignspaceApp.binaryData'] = plistlib.Data(b'<binary gunk>')
@@ -158,16 +176,21 @@ def test_fill_document(tmpdir):
])
r1.subs.append(("a", "a.alt"))
doc.addRule(r1)
- # write the document
+ # write the document; without an explicit format it will be 5.0 by default
+ doc.write(testDocPath5)
+ assert os.path.exists(testDocPath5)
+ assert_equals_test_file(testDocPath5, 'data/test_v5_original.designspace')
+ # write again with an explicit format = 4.1
+ doc.formatVersion = "4.1"
doc.write(testDocPath)
assert os.path.exists(testDocPath)
- assert_equals_test_file(testDocPath, 'data/test.designspace')
+ assert_equals_test_file(testDocPath, 'data/test_v4_original.designspace')
# import it again
new = DesignSpaceDocument()
new.read(testDocPath)
assert new.default.location == {'width': 20.0, 'weight': 0.0}
- assert new.filename == 'test.designspace'
+ assert new.filename == 'test_v4.designspace'
assert new.lib == doc.lib
assert new.instances[0].lib == doc.instances[0].lib
@@ -197,6 +220,7 @@ def test_unicodes(tmpdir):
instancePath1 = os.path.join(tmpdir, "instances", "instanceTest1.ufo")
instancePath2 = os.path.join(tmpdir, "instances", "instanceTest2.ufo")
doc = DesignSpaceDocument()
+ doc.formatVersion = "4.1" # This test about instance glyphs is deprecated in v5
# add master 1
s1 = SourceDescriptor()
s1.filename = os.path.relpath(masterPath1, os.path.dirname(testDocPath))
@@ -832,7 +856,7 @@ def test_updatePaths(tmpdir):
def test_read_with_path_object():
import pathlib
- source = (pathlib.Path(__file__) / "../data/test.designspace").resolve()
+ source = (pathlib.Path(__file__) / "../data/test_v4_original.designspace").resolve()
assert source.exists()
doc = DesignSpaceDocument()
doc.read(source)
@@ -841,7 +865,7 @@ def test_read_with_path_object():
def test_with_with_path_object(tmpdir):
import pathlib
tmpdir = str(tmpdir)
- dest = pathlib.Path(tmpdir) / "test.designspace"
+ dest = pathlib.Path(tmpdir) / "test_v4_original.designspace"
doc = DesignSpaceDocument()
doc.write(dest)
assert dest.exists()
@@ -1020,3 +1044,23 @@ def test_addRuleDescriptor(tmp_path):
# Test it doesn't crash.
ds.write(tmp_path / "test.designspace")
+
+
+def test_deepcopyExceptFonts():
+ ds = DesignSpaceDocument()
+ ds.addSourceDescriptor(font=object())
+ ds.addSourceDescriptor(font=object())
+
+ ds_copy = ds.deepcopyExceptFonts()
+
+ assert ds.tostring() == ds_copy.tostring()
+ assert ds.sources[0].font is ds_copy.sources[0].font
+ assert ds.sources[1].font is ds_copy.sources[1].font
+
+
+def test_Range_post_init():
+ # test min and max are sorted and default is clamped to either min/max
+ r = Range(minimum=2, maximum=-1, default=-2)
+ assert r.minimum == -1
+ assert r.maximum == 2
+ assert r.default == -1
diff --git a/Tests/designspaceLib/designspace_v5_test.py b/Tests/designspaceLib/designspace_v5_test.py
new file mode 100644
index 00000000..35ad29b2
--- /dev/null
+++ b/Tests/designspaceLib/designspace_v5_test.py
@@ -0,0 +1,904 @@
+import re
+import shutil
+from pathlib import Path
+
+import pytest
+from fontTools.designspaceLib import (
+ AxisDescriptor,
+ AxisLabelDescriptor,
+ DesignSpaceDocument,
+ DiscreteAxisDescriptor,
+ InstanceDescriptor,
+ LocationLabelDescriptor,
+ RangeAxisSubsetDescriptor,
+ SourceDescriptor,
+ ValueAxisSubsetDescriptor,
+ VariableFontDescriptor,
+ posix,
+)
+
+from .fixtures import datadir
+
+
+def assert_descriptors_equal(actual, expected):
+ assert len(actual) == len(expected)
+ for a, e in zip(actual, expected):
+ assert a.asdict() == e.asdict()
+
+
+def test_read_v5_document_simple(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
+
+ assert_descriptors_equal(
+ doc.axes,
+ [
+ AxisDescriptor(
+ tag="wght",
+ name="Weight",
+ minimum=200,
+ maximum=1000,
+ default=200,
+ labelNames={"en": "Wéíght", "fa-IR": "قطر"},
+ map=[
+ (200, 0),
+ (300, 100),
+ (400, 368),
+ (600, 600),
+ (700, 824),
+ (900, 1000),
+ ],
+ axisOrdering=None,
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Extra Light",
+ userMinimum=200,
+ userValue=200,
+ userMaximum=250,
+ labelNames={"de": "Extraleicht", "fr": "Extra léger"},
+ ),
+ AxisLabelDescriptor(
+ name="Light", userMinimum=250, userValue=300, userMaximum=350
+ ),
+ AxisLabelDescriptor(
+ name="Regular",
+ userMinimum=350,
+ userValue=400,
+ userMaximum=450,
+ elidable=True,
+ ),
+ AxisLabelDescriptor(
+ name="Semi Bold",
+ userMinimum=450,
+ userValue=600,
+ userMaximum=650,
+ ),
+ AxisLabelDescriptor(
+ name="Bold", userMinimum=650, userValue=700, userMaximum=850
+ ),
+ AxisLabelDescriptor(
+ name="Black", userMinimum=850, userValue=900, userMaximum=900
+ ),
+ ],
+ ),
+ AxisDescriptor(
+ tag="wdth",
+ name="Width",
+ minimum=50,
+ maximum=150,
+ default=100,
+ hidden=True,
+ labelNames={"fr": "Chasse"},
+ map=[(50, 10), (100, 20), (125, 66), (150, 990)],
+ axisOrdering=1,
+ axisLabels=[
+ AxisLabelDescriptor(name="Condensed", userValue=50),
+ AxisLabelDescriptor(
+ name="Normal", elidable=True, olderSibling=True, userValue=100
+ ),
+ AxisLabelDescriptor(name="Wide", userValue=125),
+ AxisLabelDescriptor(
+ name="Extra Wide", userValue=150, userMinimum=150
+ ),
+ ],
+ ),
+ DiscreteAxisDescriptor(
+ tag="ital",
+ name="Italic",
+ values=[0, 1],
+ default=0,
+ axisOrdering=None,
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Roman", userValue=0, elidable=True, linkedUserValue=1
+ ),
+ AxisLabelDescriptor(name="Italic", userValue=1),
+ ],
+ ),
+ ],
+ )
+
+ assert_descriptors_equal(
+ doc.locationLabels,
+ [
+ LocationLabelDescriptor(
+ name="Some Style",
+ labelNames={"fr": "Un Style"},
+ userLocation={"Weight": 300, "Width": 50, "Italic": 0},
+ ),
+ LocationLabelDescriptor(
+ name="Other", userLocation={"Weight": 700, "Width": 100, "Italic": 1}
+ ),
+ ],
+ )
+
+ assert_descriptors_equal(
+ doc.sources,
+ [
+ SourceDescriptor(
+ filename="masters/masterTest1.ufo",
+ path=posix(str((datadir / "masters/masterTest1.ufo").resolve())),
+ name="master.ufo1",
+ layerName=None,
+ location={"Italic": 0.0, "Weight": 0.0, "Width": 20.0},
+ copyLib=True,
+ copyInfo=True,
+ copyGroups=False,
+ copyFeatures=True,
+ muteKerning=False,
+ muteInfo=False,
+ mutedGlyphNames=["A", "Z"],
+ familyName="MasterFamilyName",
+ styleName="MasterStyleNameOne",
+ localisedFamilyName={"fr": "Montserrat", "ja": "モンセラート"},
+ ),
+ SourceDescriptor(
+ filename="masters/masterTest2.ufo",
+ path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
+ name="master.ufo2",
+ layerName=None,
+ location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0},
+ copyLib=False,
+ copyInfo=False,
+ copyGroups=False,
+ copyFeatures=False,
+ muteKerning=True,
+ muteInfo=False,
+ mutedGlyphNames=[],
+ familyName="MasterFamilyName",
+ styleName="MasterStyleNameTwo",
+ localisedFamilyName={},
+ ),
+ SourceDescriptor(
+ filename="masters/masterTest2.ufo",
+ path=posix(str((datadir / "masters/masterTest2.ufo").resolve())),
+ name="master.ufo2",
+ layerName="supports",
+ location={"Italic": 0.0, "Weight": 1000.0, "Width": 20.0},
+ copyLib=False,
+ copyInfo=False,
+ copyGroups=False,
+ copyFeatures=False,
+ muteKerning=False,
+ muteInfo=False,
+ mutedGlyphNames=[],
+ familyName="MasterFamilyName",
+ 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={},
+ ),
+ ],
+ )
+
+ assert_descriptors_equal(
+ doc.variableFonts,
+ [
+ VariableFontDescriptor(
+ name="Test_WghtWdth",
+ filename="Test_WghtWdth_different_from_name.ttf",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ RangeAxisSubsetDescriptor(name="Width"),
+ ],
+ lib={"com.vtt.source": "sources/vtt/Test_WghtWdth.vtt"},
+ ),
+ VariableFontDescriptor(
+ name="Test_Wght",
+ axisSubsets=[RangeAxisSubsetDescriptor(name="Weight")],
+ lib={"com.vtt.source": "sources/vtt/Test_Wght.vtt"},
+ ),
+ VariableFontDescriptor(
+ name="TestCd_Wght",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ ValueAxisSubsetDescriptor(name="Width", userValue=0),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="TestWd_Wght",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ ValueAxisSubsetDescriptor(name="Width", userValue=1000),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="TestItalic_Wght",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ ValueAxisSubsetDescriptor(name="Italic", userValue=1),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="TestRB_Wght",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(
+ name="Weight", userMinimum=400, userDefault=400, userMaximum=700
+ ),
+ ValueAxisSubsetDescriptor(name="Italic", userValue=0),
+ ],
+ ),
+ ],
+ )
+
+ assert_descriptors_equal(
+ doc.instances,
+ [
+ InstanceDescriptor(
+ filename="instances/instanceTest1.ufo",
+ path=posix(str((datadir / "instances/instanceTest1.ufo").resolve())),
+ name="instance.ufo1",
+ designLocation={"Weight": 500.0, "Width": 20.0},
+ familyName="InstanceFamilyName",
+ styleName="InstanceStyleName",
+ postScriptFontName="InstancePostscriptName",
+ styleMapFamilyName="InstanceStyleMapFamilyName",
+ styleMapStyleName="InstanceStyleMapStyleName",
+ localisedFamilyName={"fr": "Montserrat", "ja": "モンセラート"},
+ localisedStyleName={"fr": "Demigras", "ja": "半ば"},
+ localisedStyleMapFamilyName={
+ "de": "Montserrat Halbfett",
+ "ja": "モンセラート SemiBold",
+ },
+ localisedStyleMapStyleName={"de": "Standard"},
+ glyphs={"arrow": {"mute": True, "unicodes": [291, 292, 293]}},
+ lib={
+ "com.coolDesignspaceApp.binaryData": b"<binary gunk>",
+ "com.coolDesignspaceApp.specimenText": "Hamburgerwhatever",
+ },
+ ),
+ InstanceDescriptor(
+ filename="instances/instanceTest2.ufo",
+ path=posix(str((datadir / "instances/instanceTest2.ufo").resolve())),
+ name="instance.ufo2",
+ designLocation={"Weight": 500.0, "Width": (400.0, 300.0)},
+ familyName="InstanceFamilyName",
+ styleName="InstanceStyleName",
+ postScriptFontName="InstancePostscriptName",
+ styleMapFamilyName="InstanceStyleMapFamilyName",
+ styleMapStyleName="InstanceStyleMapStyleName",
+ glyphs={
+ "arrow": {
+ "unicodes": [101, 201, 301],
+ "note": "A note about this glyph",
+ "instanceLocation": {"Weight": 120.0, "Width": 100.0},
+ "masters": [
+ {
+ "font": "master.ufo1",
+ "location": {"Weight": 20.0, "Width": 20.0},
+ "glyphName": "BB",
+ },
+ {
+ "font": "master.ufo2",
+ "location": {"Weight": 900.0, "Width": 900.0},
+ "glyphName": "CC",
+ },
+ ],
+ },
+ "arrow2": {},
+ },
+ ),
+ InstanceDescriptor(
+ locationLabel="Some Style",
+ ),
+ InstanceDescriptor(
+ designLocation={"Weight": 600.0, "Width": (401.0, 420.0)},
+ ),
+ InstanceDescriptor(
+ designLocation={"Weight": 10.0, "Italic": 0.0},
+ userLocation={"Width": 100.0},
+ ),
+ InstanceDescriptor(
+ userLocation={"Weight": 300.0, "Width": 130.0, "Italic": 1.0},
+ ),
+ ],
+ )
+
+
+def test_read_v5_document_decovar(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5_decovar.designspace")
+
+ assert not doc.variableFonts
+
+ assert_descriptors_equal(
+ doc.axes,
+ [
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Inline", tag="BLDA"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Shearded", tag="TRMD"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Rounded Slab", tag="TRMC"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Stripes", tag="SKLD"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Worm Terminal", tag="TRML"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Inline Skeleton", tag="SKLA"
+ ),
+ AxisDescriptor(
+ default=0,
+ maximum=1000,
+ minimum=0,
+ name="Open Inline Terminal",
+ tag="TRMF",
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Inline Terminal", tag="TRMK"
+ ),
+ AxisDescriptor(default=0, maximum=1000, minimum=0, name="Worm", tag="BLDB"),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Weight", tag="WMX2"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Flared", tag="TRMB"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Rounded", tag="TRMA"
+ ),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Worm Skeleton", tag="SKLB"
+ ),
+ AxisDescriptor(default=0, maximum=1000, minimum=0, name="Slab", tag="TRMG"),
+ AxisDescriptor(
+ default=0, maximum=1000, minimum=0, name="Bifurcated", tag="TRME"
+ ),
+ ],
+ )
+
+ assert_descriptors_equal(
+ doc.locationLabels,
+ [
+ LocationLabelDescriptor(name="Default", elidable=True, userLocation={}),
+ LocationLabelDescriptor(
+ name="Open", userLocation={"Inline": 1000}, labelNames={"de": "Offen"}
+ ),
+ LocationLabelDescriptor(name="Worm", userLocation={"Worm": 1000}),
+ LocationLabelDescriptor(
+ name="Checkered", userLocation={"Inline Skeleton": 1000}
+ ),
+ LocationLabelDescriptor(
+ name="Checkered Reverse", userLocation={"Inline Terminal": 1000}
+ ),
+ LocationLabelDescriptor(name="Striped", userLocation={"Stripes": 500}),
+ LocationLabelDescriptor(name="Rounded", userLocation={"Rounded": 1000}),
+ LocationLabelDescriptor(name="Flared", userLocation={"Flared": 1000}),
+ LocationLabelDescriptor(
+ name="Flared Open",
+ userLocation={"Inline Skeleton": 1000, "Flared": 1000},
+ ),
+ LocationLabelDescriptor(
+ name="Rounded Slab", userLocation={"Rounded Slab": 1000}
+ ),
+ LocationLabelDescriptor(name="Sheared", userLocation={"Shearded": 1000}),
+ LocationLabelDescriptor(
+ name="Bifurcated", userLocation={"Bifurcated": 1000}
+ ),
+ LocationLabelDescriptor(
+ name="Inline",
+ userLocation={"Inline Skeleton": 500, "Open Inline Terminal": 500},
+ ),
+ LocationLabelDescriptor(name="Slab", userLocation={"Slab": 1000}),
+ LocationLabelDescriptor(name="Contrast", userLocation={"Weight": 1000}),
+ LocationLabelDescriptor(
+ name="Fancy",
+ userLocation={"Inline Skeleton": 1000, "Flared": 1000, "Weight": 1000},
+ ),
+ LocationLabelDescriptor(
+ name="Mayhem",
+ userLocation={
+ "Inline Skeleton": 1000,
+ "Worm Skeleton": 1000,
+ "Rounded": 500,
+ "Flared": 500,
+ "Rounded Slab": 750,
+ "Bifurcated": 500,
+ "Open Inline Terminal": 250,
+ "Slab": 750,
+ "Inline Terminal": 250,
+ "Worm Terminal": 250,
+ "Weight": 750,
+ "Worm": 1000,
+ },
+ ),
+ ],
+ )
+
+ assert [i.locationLabel for i in doc.instances] == [
+ "Default",
+ "Open",
+ "Worm",
+ "Checkered",
+ "Checkered Reverse",
+ "Striped",
+ "Rounded",
+ "Flared",
+ "Flared Open",
+ "Rounded Slab",
+ "Sheared",
+ "Bifurcated",
+ "Inline",
+ "Slab",
+ "Contrast",
+ "Fancy",
+ "Mayhem",
+ ]
+
+
+def test_read_v5_document_discrete(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5_discrete.designspace")
+
+ assert not doc.locationLabels
+ assert not doc.variableFonts
+
+ assert_descriptors_equal(
+ doc.axes,
+ [
+ DiscreteAxisDescriptor(
+ default=400,
+ values=[400, 700, 900],
+ name="Weight",
+ tag="wght",
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Regular",
+ userValue=400,
+ elidable=True,
+ linkedUserValue=700,
+ ),
+ AxisLabelDescriptor(name="Bold", userValue=700),
+ AxisLabelDescriptor(name="Black", userValue=900),
+ ],
+ ),
+ DiscreteAxisDescriptor(
+ default=100,
+ values=[75, 100],
+ name="Width",
+ tag="wdth",
+ axisLabels=[
+ AxisLabelDescriptor(name="Narrow", userValue=75),
+ AxisLabelDescriptor(name="Normal", userValue=100, elidable=True),
+ ],
+ ),
+ DiscreteAxisDescriptor(
+ default=0,
+ values=[0, 1],
+ name="Italic",
+ tag="ital",
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Roman", userValue=0, elidable=True, linkedUserValue=1
+ ),
+ AxisLabelDescriptor(name="Italic", userValue=1),
+ ],
+ ),
+ ],
+ )
+
+
+def test_read_v5_document_aktiv(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5_aktiv.designspace")
+
+ assert not doc.locationLabels
+
+ assert_descriptors_equal(
+ doc.axes,
+ [
+ AxisDescriptor(
+ tag="wght",
+ name="Weight",
+ minimum=100,
+ default=400,
+ maximum=900,
+ map=[
+ (100, 22),
+ (200, 38),
+ (300, 57),
+ (400, 84),
+ (500, 98),
+ (600, 115),
+ (700, 133),
+ (800, 158),
+ (900, 185),
+ ],
+ axisOrdering=1,
+ axisLabels=[
+ AxisLabelDescriptor(name="Hair", userValue=100),
+ AxisLabelDescriptor(userValue=200, name="Thin"),
+ AxisLabelDescriptor(userValue=300, name="Light"),
+ AxisLabelDescriptor(
+ userValue=400,
+ name="Regular",
+ elidable=True,
+ linkedUserValue=700,
+ ),
+ AxisLabelDescriptor(userValue=500, name="Medium"),
+ AxisLabelDescriptor(userValue=600, name="SemiBold"),
+ AxisLabelDescriptor(userValue=700, name="Bold"),
+ AxisLabelDescriptor(userValue=800, name="XBold"),
+ AxisLabelDescriptor(userValue=900, name="Black"),
+ ],
+ ),
+ AxisDescriptor(
+ tag="wdth",
+ name="Width",
+ minimum=75,
+ default=100,
+ maximum=125,
+ axisOrdering=0,
+ axisLabels=[
+ AxisLabelDescriptor(name="Cd", userValue=75),
+ AxisLabelDescriptor(name="Normal", elidable=True, userValue=100),
+ AxisLabelDescriptor(name="Ex", userValue=125),
+ ],
+ ),
+ AxisDescriptor(
+ tag="ital",
+ name="Italic",
+ minimum=0,
+ default=0,
+ maximum=1,
+ axisOrdering=2,
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Upright", userValue=0, elidable=True, linkedUserValue=1
+ ),
+ AxisLabelDescriptor(name="Italic", userValue=1),
+ ],
+ ),
+ ],
+ )
+
+ assert_descriptors_equal(
+ doc.variableFonts,
+ [
+ VariableFontDescriptor(
+ name="AktivGroteskVF_WghtWdthItal",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ RangeAxisSubsetDescriptor(name="Width"),
+ RangeAxisSubsetDescriptor(name="Italic"),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="AktivGroteskVF_WghtWdth",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ RangeAxisSubsetDescriptor(name="Width"),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="AktivGroteskVF_Wght",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="AktivGroteskVF_Italics_WghtWdth",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ RangeAxisSubsetDescriptor(name="Width"),
+ ValueAxisSubsetDescriptor(name="Italic", userValue=1),
+ ],
+ ),
+ VariableFontDescriptor(
+ name="AktivGroteskVF_Italics_Wght",
+ axisSubsets=[
+ RangeAxisSubsetDescriptor(name="Weight"),
+ ValueAxisSubsetDescriptor(name="Italic", userValue=1),
+ ],
+ ),
+ ],
+ )
+
+
+@pytest.fixture
+def map_doc():
+ """Generate a document with a few axes to test the mapping functions"""
+ doc = DesignSpaceDocument()
+ doc.addAxis(
+ AxisDescriptor(
+ tag="wght",
+ name="Weight",
+ minimum=100,
+ maximum=900,
+ default=100,
+ map=[(100, 10), (900, 90)],
+ )
+ )
+ doc.addAxis(
+ AxisDescriptor(
+ tag="wdth",
+ name="Width",
+ minimum=75,
+ maximum=200,
+ default=100,
+ map=[(75, 7500), (100, 10000), (200, 20000)],
+ )
+ )
+ doc.addAxis(
+ AxisDescriptor(tag="CUST", name="Custom", minimum=1, maximum=2, default=1.5)
+ )
+ doc.addLocationLabel(
+ LocationLabelDescriptor(
+ name="Wonky", userLocation={"Weight": 800, "Custom": 1.2}
+ )
+ )
+ return doc
+
+
+def test_doc_location_map_forward(map_doc: DesignSpaceDocument):
+ assert map_doc.map_forward({"Weight": 400, "Width": 150, "Custom": 2}) == {
+ "Weight": 40,
+ "Width": 15000,
+ "Custom": 2,
+ }, "The mappings should be used to compute the design locations"
+ assert map_doc.map_forward({"Weight": 400}) == {
+ "Weight": 40,
+ "Width": 10000,
+ "Custom": 1.5,
+ }, "Missing user locations should be assumed equal to the axis's default"
+
+
+def test_doc_location_map_backward(map_doc: DesignSpaceDocument):
+ assert map_doc.map_backward({"Weight": 40, "Width": 15000, "Custom": 2}) == {
+ "Weight": 400,
+ "Width": 150,
+ "Custom": 2,
+ }, "The mappings should be used to compute the user locations"
+ assert map_doc.map_backward({"Weight": 40}) == {
+ "Weight": 400,
+ "Width": 100,
+ "Custom": 1.5,
+ }, "Missing design locations should be assumed equal to the axis's default"
+ assert map_doc.map_backward(
+ {"Weight": (40, 50), "Width": (15000, 100000), "Custom": (2, 1.5)}
+ ) == {
+ "Weight": 400,
+ "Width": 150,
+ "Custom": 2,
+ }, "Only the xvalue of anisotropic locations is used"
+
+
+def test_instance_location_from_label(map_doc):
+ inst = InstanceDescriptor(locationLabel="Wonky")
+ assert inst.getFullUserLocation(map_doc) == {
+ "Weight": 800,
+ "Width": 100,
+ "Custom": 1.2,
+ }, "an instance with a locationLabel uses the user location from that label, empty values on the label use axis defaults"
+ assert inst.getFullDesignLocation(map_doc) == {
+ "Weight": 80,
+ "Width": 10000,
+ "Custom": 1.2,
+ }, "an instance with a locationLabel computes the design location from that label, empty values on the label use axis defaults"
+
+ inst = InstanceDescriptor(locationLabel="Wonky", userLocation={"Width": 200})
+ assert inst.getFullUserLocation(map_doc) == {
+ "Weight": 800,
+ "Width": 100,
+ "Custom": 1.2,
+ }, "an instance with a locationLabel uses the user location from that label, other location values are ignored"
+ assert inst.getFullDesignLocation(map_doc) == {
+ "Weight": 80,
+ "Width": 10000,
+ "Custom": 1.2,
+ }, "an instance with a locationLabel computes the design location from that label, other location values are ignored"
+
+
+def test_instance_location_no_data(map_doc):
+ inst = InstanceDescriptor()
+ assert inst.getFullUserLocation(map_doc) == {
+ "Weight": 100,
+ "Width": 100,
+ "Custom": 1.5,
+ }, "an instance without any location data has the default user location"
+ assert inst.getFullDesignLocation(map_doc) == {
+ "Weight": 10,
+ "Width": 10000,
+ "Custom": 1.5,
+ }, "an instance without any location data has the default design location"
+
+
+def test_instance_location_design_first(map_doc):
+ inst = InstanceDescriptor(
+ designLocation={"Weight": (60, 61), "Width": 11000, "Custom": 1.2},
+ userLocation={"Weight": 700, "Width": 180, "Custom": 1.4},
+ )
+ assert inst.getFullUserLocation(map_doc) == {
+ "Weight": 600,
+ "Width": 110,
+ "Custom": 1.2,
+ }, "when both design and user location data are provided, design wins"
+ assert inst.getFullDesignLocation(map_doc) == {
+ "Weight": (60, 61),
+ "Width": 11000,
+ "Custom": 1.2,
+ }, "when both design and user location data are provided, design wins (incl. anisotropy)"
+
+
+def test_instance_location_mix(map_doc):
+ inst = InstanceDescriptor(
+ designLocation={"Weight": (60, 61)},
+ userLocation={"Width": 180},
+ )
+ assert inst.getFullUserLocation(map_doc) == {
+ "Weight": 600,
+ "Width": 180,
+ "Custom": 1.5,
+ }, "instance location is a mix of design and user locations"
+ assert inst.getFullDesignLocation(map_doc) == {
+ "Weight": (60, 61),
+ "Width": 18000,
+ "Custom": 1.5,
+ }, "instance location is a mix of design and user location"
+
+
+@pytest.mark.parametrize(
+ "filename",
+ [
+ "test_v4_original.designspace",
+ "test_v5_original.designspace",
+ "test_v5_aktiv.designspace",
+ "test_v5_decovar.designspace",
+ "test_v5_discrete.designspace",
+ "test_v5_sourceserif.designspace",
+ "test_v5.designspace",
+ ],
+)
+def test_roundtrip(tmpdir, datadir, filename):
+ test_file = datadir / filename
+ output_path = tmpdir / filename
+ # Move the file to the tmpdir so that the filenames stay the same
+ # (they're relative to the file's path)
+ shutil.copy(test_file, output_path)
+ doc = DesignSpaceDocument.fromfile(output_path)
+ doc.write(output_path)
+ # The input XML has comments and empty lines for documentation purposes
+ xml = test_file.read_text(encoding="utf-8")
+ xml = re.sub(
+ r"<!-- ROUNDTRIP_TEST_REMOVE_ME_BEGIN -->(.|\n)*?<!-- ROUNDTRIP_TEST_REMOVE_ME_END -->",
+ "",
+ xml,
+ )
+ xml = re.sub(r"<!--(.|\n)*?-->", "", xml)
+ xml = re.sub(r"\s*\n+", "\n", xml)
+ assert output_path.read_text(encoding="utf-8") == xml
+
+
+def test_using_v5_features_upgrades_format(tmpdir, datadir):
+ test_file = datadir / "test_v4_original.designspace"
+ output_4_path = tmpdir / "test_v4.designspace"
+ output_5_path = tmpdir / "test_v5.designspace"
+ shutil.copy(test_file, output_4_path)
+ doc = DesignSpaceDocument.fromfile(output_4_path)
+ doc.write(output_4_path)
+ assert 'format="4.1"' in output_4_path.read_text(encoding="utf-8")
+ doc.addVariableFont(VariableFontDescriptor(name="TestVF"))
+ doc.write(output_5_path)
+ assert 'format="5.0"' in output_5_path.read_text(encoding="utf-8")
+
+
+def test_addAxisDescriptor_discrete():
+ ds = DesignSpaceDocument()
+
+ axis = ds.addAxisDescriptor(
+ name="Italic",
+ tag="ital",
+ values=[0, 1],
+ default=0,
+ hidden=True,
+ map=[(0, -12), (1, 0)],
+ axisOrdering=3,
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Roman",
+ userValue=0,
+ elidable=True,
+ olderSibling=True,
+ linkedUserValue=1,
+ labelNames={"fr": "Romain"},
+ )
+ ],
+ )
+
+ assert ds.axes[0] is axis
+ assert_descriptors_equal(
+ [axis],
+ [
+ DiscreteAxisDescriptor(
+ tag="ital",
+ name="Italic",
+ values=[0, 1],
+ default=0,
+ hidden=True,
+ map=[(0, -12), (1, 0)],
+ axisOrdering=3,
+ axisLabels=[
+ AxisLabelDescriptor(
+ name="Roman",
+ userValue=0,
+ elidable=True,
+ olderSibling=True,
+ linkedUserValue=1,
+ labelNames={"fr": "Romain"},
+ )
+ ],
+ )
+ ],
+ )
+
+
+def test_addLocationLabelDescriptor():
+ ds = DesignSpaceDocument()
+
+ label = ds.addLocationLabelDescriptor(
+ name="Somewhere",
+ userLocation={},
+ elidable=True,
+ olderSibling=True,
+ labelNames={"fr": "Quelque part"},
+ )
+
+ assert ds.locationLabels[0] is label
+ assert_descriptors_equal(
+ [label],
+ [
+ LocationLabelDescriptor(
+ name="Somewhere",
+ userLocation={},
+ elidable=True,
+ olderSibling=True,
+ labelNames={"fr": "Quelque part"},
+ )
+ ],
+ )
+
+
+def test_addVariableFontDescriptor():
+ ds = DesignSpaceDocument()
+
+ vf = ds.addVariableFontDescriptor(name="TestVF", filename="TestVF.ttf")
+
+ assert ds.variableFonts[0] is vf
+ assert_descriptors_equal(
+ [vf], [VariableFontDescriptor(name="TestVF", filename="TestVF.ttf")]
+ )
diff --git a/Tests/designspaceLib/fixtures.py b/Tests/designspaceLib/fixtures.py
new file mode 100644
index 00000000..66041bed
--- /dev/null
+++ b/Tests/designspaceLib/fixtures.py
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+import pytest
+
+
+@pytest.fixture
+def datadir():
+ return Path(__file__).parent / "data"
diff --git a/Tests/designspaceLib/split_test.py b/Tests/designspaceLib/split_test.py
new file mode 100644
index 00000000..8708f704
--- /dev/null
+++ b/Tests/designspaceLib/split_test.py
@@ -0,0 +1,150 @@
+import shutil
+from pathlib import Path
+
+import pytest
+from fontTools.designspaceLib import DesignSpaceDocument
+from fontTools.designspaceLib.split import splitInterpolable, splitVariableFonts, convert5to4
+
+from .fixtures import datadir
+
+UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING = False
+
+
+@pytest.mark.parametrize(
+ "test_ds,expected_interpolable_spaces",
+ [
+ (
+ "test_v5_aktiv.designspace",
+ [
+ (
+ {},
+ {
+ "AktivGroteskVF_Italics_Wght",
+ "AktivGroteskVF_Italics_WghtWdth",
+ "AktivGroteskVF_Wght",
+ "AktivGroteskVF_WghtWdth",
+ "AktivGroteskVF_WghtWdthItal",
+ },
+ )
+ ],
+ ),
+ (
+ "test_v5_sourceserif.designspace",
+ [
+ (
+ {"italic": 0},
+ {"SourceSerif4Variable-Roman"},
+ ),
+ (
+ {"italic": 1},
+ {"SourceSerif4Variable-Italic"},
+ ),
+ ],
+ ),
+ (
+ "test_v5_MutatorSans_and_Serif.designspace",
+ [
+ (
+ {"serif": 0},
+ {
+ "MutatorSansVariable_Weight_Width",
+ "MutatorSansVariable_Weight",
+ "MutatorSansVariable_Width",
+ },
+ ),
+ (
+ {"serif": 1},
+ {
+ "MutatorSerifVariable_Width",
+ },
+ ),
+ ],
+ ),
+ ],
+)
+def test_split(datadir, tmpdir, test_ds, expected_interpolable_spaces):
+ data_in = datadir / test_ds
+ temp_in = Path(tmpdir) / test_ds
+ shutil.copy(data_in, temp_in)
+ doc = DesignSpaceDocument.fromfile(temp_in)
+
+ for i, (location, sub_doc) in enumerate(splitInterpolable(doc)):
+ expected_location, expected_vf_names = expected_interpolable_spaces[i]
+ assert location == expected_location
+ vfs = list(splitVariableFonts(sub_doc))
+ assert expected_vf_names == set(vf[0] for vf in vfs)
+
+ loc_str = "_".join(f"{name}_{value}"for name, value in sorted(location.items()))
+ data_out = datadir / "split_output" / f"{temp_in.stem}_{loc_str}.designspace"
+ temp_out = Path(tmpdir) / "out" / f"{temp_in.stem}_{loc_str}.designspace"
+ temp_out.parent.mkdir(exist_ok=True)
+ sub_doc.write(temp_out)
+
+ if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
+ data_out.write_text(temp_out.read_text(encoding="utf-8"), encoding="utf-8")
+ else:
+ assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
+ encoding="utf-8"
+ )
+
+ for vf_name, vf_doc in vfs:
+ data_out = (datadir / "split_output" / vf_name).with_suffix(".designspace")
+ temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace")
+ temp_out.parent.mkdir(exist_ok=True)
+ vf_doc.write(temp_out)
+
+ if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
+ data_out.write_text(
+ temp_out.read_text(encoding="utf-8"), encoding="utf-8"
+ )
+ else:
+ assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
+ encoding="utf-8"
+ )
+
+
+
+
+@pytest.mark.parametrize(
+ "test_ds,expected_vfs",
+ [
+ (
+ "test_v5_aktiv.designspace",
+ {
+ "AktivGroteskVF_Italics_Wght",
+ "AktivGroteskVF_Italics_WghtWdth",
+ "AktivGroteskVF_Wght",
+ "AktivGroteskVF_WghtWdth",
+ "AktivGroteskVF_WghtWdthItal",
+ },
+ ),
+ (
+ "test_v5_sourceserif.designspace",
+ {
+ "SourceSerif4Variable-Italic",
+ "SourceSerif4Variable-Roman",
+ },
+ ),
+ ],
+)
+def test_convert5to4(datadir, tmpdir, test_ds, expected_vfs):
+ data_in = datadir / test_ds
+ temp_in = tmpdir / test_ds
+ shutil.copy(data_in, temp_in)
+ doc = DesignSpaceDocument.fromfile(temp_in)
+
+ variable_fonts = convert5to4(doc)
+
+ assert variable_fonts.keys() == expected_vfs
+ for vf_name, vf in variable_fonts.items():
+ data_out = (datadir / "convert5to4_output" / vf_name).with_suffix(".designspace")
+ temp_out = (Path(tmpdir) / "out" / vf_name).with_suffix(".designspace")
+ temp_out.parent.mkdir(exist_ok=True)
+ vf.write(temp_out)
+
+ if UPDATE_REFERENCE_OUT_FILES_INSTEAD_OF_TESTING:
+ data_out.write_text(temp_out.read_text(encoding="utf-8"), encoding="utf-8")
+ else:
+ assert data_out.read_text(encoding="utf-8") == temp_out.read_text(
+ encoding="utf-8"
+ )
diff --git a/Tests/designspaceLib/statNames_test.py b/Tests/designspaceLib/statNames_test.py
new file mode 100644
index 00000000..99d1c7fa
--- /dev/null
+++ b/Tests/designspaceLib/statNames_test.py
@@ -0,0 +1,78 @@
+from fontTools.designspaceLib import DesignSpaceDocument
+from fontTools.designspaceLib.statNames import StatNames, getStatNames
+
+from .fixtures import datadir
+
+
+def test_instance_getStatNames(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5_sourceserif.designspace")
+
+ assert getStatNames(doc, doc.instances[0].getFullUserLocation(doc)) == StatNames(
+ familyNames={"en": "Source Serif 4"},
+ styleNames={"en": "Caption ExtraLight"},
+ postScriptFontName="SourceSerif4-CaptionExtraLight",
+ styleMapFamilyNames={"en": "Source Serif 4 Caption ExtraLight"},
+ styleMapStyleName="regular",
+ )
+
+
+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(
+ familyNames={
+ "en": "MasterFamilyName",
+ "fr": "Montserrat",
+ "ja": "モンセラート",
+ },
+ styleNames={
+ "fr": "Wide Extra léger Italic",
+ "de": "Wide Extraleicht Italic",
+ "en": "Wide Extra Light Italic",
+ },
+ postScriptFontName="MasterFamilyName-WideExtraLightItalic",
+ styleMapFamilyNames={
+ "en": "MasterFamilyName Wide Extra Light",
+ "fr": "Montserrat Wide Extra léger",
+ "de": "MasterFamilyName Wide Extraleicht",
+ "ja": "モンセラート Wide Extra Light",
+ },
+ styleMapStyleName="italic",
+ )
+
+
+def test_detect_ribbi_aktiv(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5_aktiv.designspace")
+
+ assert getStatNames(doc, {"Weight": 600, "Width": 125, "Italic": 1}) == StatNames(
+ familyNames={"en": "Aktiv Grotesk"},
+ styleNames={"en": "Ex SemiBold Italic"},
+ postScriptFontName="AktivGrotesk-ExSemiBoldItalic",
+ styleMapFamilyNames={"en": "Aktiv Grotesk Ex SemiBold"},
+ styleMapStyleName="italic",
+ )
+
+ assert getStatNames(doc, {"Weight": 700, "Width": 75, "Italic": 1}) == StatNames(
+ familyNames={"en": "Aktiv Grotesk"},
+ styleNames={"en": "Cd Bold Italic"},
+ postScriptFontName="AktivGrotesk-CdBoldItalic",
+ 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/data/spec6h_ii.ttx b/Tests/feaLib/data/spec6h_ii.ttx
index 2f0efc6f..e8ec85f2 100644
--- a/Tests/feaLib/data/spec6h_ii.ttx
+++ b/Tests/feaLib/data/spec6h_ii.ttx
@@ -112,23 +112,25 @@
</MarkBasePos>
</Lookup>
<Lookup index="2">
- <LookupType value="7"/>
+ <LookupType value="8"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ContextPos index="0" Format="3">
- <!-- GlyphCount=3 -->
- <!-- PosCount=2 -->
- <Coverage index="0">
+ <ChainContextPos index="0" Format="3">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=3 -->
+ <InputCoverage index="0">
<Glyph value="T"/>
- </Coverage>
- <Coverage index="1">
+ </InputCoverage>
+ <InputCoverage index="1">
<Glyph value="c"/>
<Glyph value="o"/>
- </Coverage>
- <Coverage index="2">
+ </InputCoverage>
+ <InputCoverage index="2">
<Glyph value="grave"/>
<Glyph value="acute"/>
- </Coverage>
+ </InputCoverage>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- PosCount=2 -->
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
@@ -137,7 +139,7 @@
<SequenceIndex value="2"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </ContextPos>
+ </ChainContextPos>
</Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/feaLib/data/spec6h_iii_3d.ttx b/Tests/feaLib/data/spec6h_iii_3d.ttx
index 2335dd0b..a608f0e6 100644
--- a/Tests/feaLib/data/spec6h_iii_3d.ttx
+++ b/Tests/feaLib/data/spec6h_iii_3d.ttx
@@ -30,23 +30,25 @@
<LookupList>
<!-- LookupCount=2 -->
<Lookup index="0">
- <LookupType value="7"/>
+ <LookupType value="8"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ContextPos index="0" Format="3">
- <!-- GlyphCount=2 -->
- <!-- PosCount=1 -->
- <Coverage index="0">
+ <ChainContextPos index="0" Format="3">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=2 -->
+ <InputCoverage index="0">
<Glyph value="L"/>
- </Coverage>
- <Coverage index="1">
+ </InputCoverage>
+ <InputCoverage index="1">
<Glyph value="quoteright"/>
- </Coverage>
+ </InputCoverage>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- PosCount=1 -->
<PosLookupRecord index="0">
<SequenceIndex value="1"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </ContextPos>
+ </ChainContextPos>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
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/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py
index 6368cb87..775e94d9 100644
--- a/Tests/fontBuilder/fontBuilder_test.py
+++ b/Tests/fontBuilder/fontBuilder_test.py
@@ -1,13 +1,13 @@
import os
import pytest
-import re
from fontTools.ttLib import TTFont
from fontTools.pens.ttGlyphPen import TTGlyphPen
from fontTools.pens.t2CharStringPen import T2CharStringPen
from fontTools.fontBuilder import FontBuilder
from fontTools.ttLib.tables.TupleVariation import TupleVariation
from fontTools.misc.psCharStrings import T2CharString
+from fontTools.misc.testTools import stripVariableItemsFromTTX
def getTestData(fileName, mode="r"):
@@ -16,16 +16,6 @@ def getTestData(fileName, mode="r"):
return f.read()
-def strip_VariableItems(string):
- # ttlib changes with the fontTools version
- string = re.sub(' ttLibVersion=".*"', '', string)
- # head table checksum and creation and mod date changes with each save.
- string = re.sub('<checkSumAdjustment value="[^"]+"/>', '', string)
- string = re.sub('<modified value="[^"]+"/>', '', string)
- string = re.sub('<created value="[^"]+"/>', '', string)
- return string
-
-
def drawTestGlyph(pen):
pen.moveTo((100, 100))
pen.lineTo((100, 1000))
@@ -91,8 +81,8 @@ def _verifyOutput(outPath, tables=None):
f = TTFont(outPath)
f.saveXML(outPath + ".ttx", tables=tables)
with open(outPath + ".ttx") as f:
- testData = strip_VariableItems(f.read())
- refData = strip_VariableItems(getTestData(os.path.basename(outPath) + ".ttx"))
+ testData = stripVariableItemsFromTTX(f.read())
+ refData = stripVariableItemsFromTTX(getTestData(os.path.basename(outPath) + ".ttx"))
assert refData == testData
diff --git a/Tests/merge/data/CFFFont_expected.ttx b/Tests/merge/data/CFFFont_expected.ttx
index 6e0acc22..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"/>
@@ -834,7 +834,7 @@
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
will be recalculated by the compiler -->
<version value="4"/>
- <xAvgCharWidth value="505"/>
+ <xAvgCharWidth value="501"/>
<usWeightClass value="400"/>
<usWidthClass value="5"/>
<fsType value="00000000 00000000"/>
@@ -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/configTools_test.py b/Tests/misc/configTools_test.py
new file mode 100644
index 00000000..94afb233
--- /dev/null
+++ b/Tests/misc/configTools_test.py
@@ -0,0 +1,80 @@
+import dataclasses
+import pytest
+
+from fontTools.misc.configTools import (
+ AbstractConfig,
+ Option,
+ Options,
+ ConfigUnknownOptionError,
+)
+
+
+def test_can_create_custom_config_system():
+ class MyConfig(AbstractConfig):
+ options = Options()
+
+ MyConfig.register_option(
+ "test:option_name",
+ "This is an option",
+ 0,
+ int,
+ lambda v: isinstance(v, int),
+ )
+
+ cfg = MyConfig({"test:option_name": "10"}, parse_values=True)
+
+ assert 10 == cfg["test:option_name"]
+
+ # This config is independent from "the" fontTools config
+ with pytest.raises(ConfigUnknownOptionError):
+ MyConfig({"fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL": 4})
+
+ # Test the repr()
+ assert repr(cfg) == "MyConfig({'test:option_name': 10})"
+
+ # Test the skip_unknown param: just check that the following does not raise
+ MyConfig({"test:unknown": "whatever"}, skip_unknown=True)
+
+ # Test that it raises on unknown option
+ with pytest.raises(ConfigUnknownOptionError):
+ cfg.get("test:unknown")
+
+
+def test_options_are_unique():
+ class MyConfig(AbstractConfig):
+ options = Options()
+
+ opt1 = MyConfig.register_option("test:OPT_1", "", "foo", str, any)
+ cfg = MyConfig({opt1: "bar"})
+ assert cfg[opt1] == "bar"
+
+ opt2 = Option("test:OPT_1", "", "foo", str, any)
+
+ assert dataclasses.asdict(opt1) == dataclasses.asdict(opt2)
+ assert opt1 != opt2
+
+ with pytest.raises(ConfigUnknownOptionError):
+ cfg.get(opt2)
+ with pytest.raises(ConfigUnknownOptionError):
+ cfg.set(opt2, "bar")
+
+
+def test_optional_bool():
+ for v in ("yes", "YES", "Yes", "1", "True", "true", "TRUE"):
+ assert Option.parse_optional_bool(v) is True
+
+ for v in ("no", "NO", "No", "0", "False", "false", "FALSE"):
+ assert Option.parse_optional_bool(v) is False
+
+ for v in ("auto", "AUTO", "Auto", "None", "none", "NONE"):
+ assert Option.parse_optional_bool(v) is None
+
+ with pytest.raises(ValueError, match="invalid optional bool"):
+ Option.parse_optional_bool("foobar")
+
+ assert Option.validate_optional_bool(True)
+ assert Option.validate_optional_bool(False)
+ assert Option.validate_optional_bool(None)
+ assert not Option.validate_optional_bool(1)
+ assert not Option.validate_optional_bool(0)
+ assert not Option.validate_optional_bool("1")
diff --git a/Tests/misc/psCharStrings_test.py b/Tests/misc/psCharStrings_test.py
index 47ff4fda..5e36fe73 100644
--- a/Tests/misc/psCharStrings_test.py
+++ b/Tests/misc/psCharStrings_test.py
@@ -8,6 +8,7 @@ from fontTools.misc.psCharStrings import (
read_fixed1616,
read_realNumber,
)
+from fontTools.pens.recordingPen import RecordingPen
import unittest
@@ -158,6 +159,14 @@ class T2CharStringTest(unittest.TestCase):
self.assertNotIsInstance(expected_arg, str)
self.assertAlmostEqual(arg, expected_arg)
+ def test_pen_closePath(self):
+ # Test CFF2/T2 charstring: it does NOT end in "endchar"
+ # https://github.com/fonttools/fonttools/issues/2455
+ cs = self.stringToT2CharString("100 100 rmoveto -50 -150 200 0 -50 150 rrcurveto")
+ pen = RecordingPen()
+ cs.draw(pen)
+ self.assertEqual(pen.value[-1], ('closePath', ()))
+
if __name__ == "__main__":
import sys
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/otlLib/optimize_test.py b/Tests/otlLib/optimize_test.py
index 40cf389e..a2e43322 100644
--- a/Tests/otlLib/optimize_test.py
+++ b/Tests/otlLib/optimize_test.py
@@ -1,17 +1,15 @@
+import contextlib
import logging
+import os
from pathlib import Path
from subprocess import run
-import contextlib
-import os
from typing import List, Optional, Tuple
-from fontTools.ttLib import TTFont
import pytest
-
from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
from fontTools.fontBuilder import FontBuilder
-
-from fontTools.ttLib.tables.otBase import OTTableWriter, ValueRecord
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.tables.otBase import OTTableWriter
def test_main(tmpdir: Path):
@@ -34,7 +32,7 @@ def test_main(tmpdir: Path):
[
"fonttools",
"otlLib.optimize",
- "--gpos-compact-mode",
+ "--gpos-compression-level",
"5",
str(input),
"-o",
@@ -127,9 +125,9 @@ def get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str
@pytest.mark.parametrize(
- ("blocks", "mode", "expected_subtables", "expected_bytes"),
+ ("blocks", "level", "expected_subtables", "expected_bytes"),
[
- # Mode = 0 = no optimization leads to 650 bytes of GPOS
+ # Level = 0 = no optimization leads to 650 bytes of GPOS
([(15, 3), (2, 10)], None, 1, 602),
# Optimization level 1 recognizes the 2 blocks and splits into 2
# subtables = adds 1 subtable leading to a size reduction of
@@ -143,13 +141,13 @@ def get_kerning_by_blocks(blocks: List[Tuple[int, int]]) -> Tuple[List[str], str
([(4, 4) for _ in range(20)], 9, 20, 1886),
# On a fully occupied kerning matrix, even the strategy 9 doesn't
# split anything.
- ([(10, 10)], 9, 1, 304)
+ ([(10, 10)], 9, 1, 304),
],
)
def test_optimization_mode(
caplog,
blocks: List[Tuple[int, int]],
- mode: Optional[int],
+ level: Optional[int],
expected_subtables: int,
expected_bytes: int,
):
@@ -161,15 +159,10 @@ def test_optimization_mode(
glyphs, features = get_kerning_by_blocks(blocks)
glyphs = [".notdef space"] + glyphs
- env = {}
- if mode is not None:
- # NOTE: activating this optimization via the environment variable is
- # experimental and may not be supported once an alternative mechanism
- # is in place. See: https://github.com/fonttools/fonttools/issues/2349
- env["FONTTOOLS_GPOS_COMPACT_MODE"] = str(mode)
- with set_env(**env):
- fb = FontBuilder(1000)
- fb.setupGlyphOrder(glyphs)
- addOpenTypeFeaturesFromString(fb.font, features)
- assert expected_subtables == count_pairpos_subtables(fb.font)
- assert expected_bytes == count_pairpos_bytes(fb.font)
+ fb = FontBuilder(1000)
+ if level is not None:
+ fb.font.cfg["fontTools.otlLib.optimize.gpos:COMPRESSION_LEVEL"] = level
+ fb.setupGlyphOrder(glyphs)
+ addOpenTypeFeaturesFromString(fb.font, features)
+ assert expected_subtables == count_pairpos_subtables(fb.font)
+ assert expected_bytes == count_pairpos_bytes(fb.font)
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_harfbuzz_repacker.ttx b/Tests/subset/data/expect_harfbuzz_repacker.ttx
new file mode 100644
index 00000000..aeebe7ba
--- /dev/null
+++ b/Tests/subset/data/expect_harfbuzz_repacker.ttx
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="OTTO" ttLibVersion="4.29">
+
+ <GSUB>
+ <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="hang"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="1"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=2 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="aalt"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="jp90"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=2 -->
+ <Lookup index="0">
+ <LookupType value="3"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <AlternateSubst index="0">
+ <AlternateSet glyph="cid12223">
+ <Alternate glyph="cid62031"/>
+ <Alternate glyph="cid62033"/>
+ <Alternate glyph="cid61789"/>
+ </AlternateSet>
+ <AlternateSet glyph="cid61789">
+ <Alternate glyph="cid62031"/>
+ <Alternate glyph="cid62033"/>
+ <Alternate glyph="cid12223"/>
+ </AlternateSet>
+ </AlternateSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="cid12223" out="cid61789"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/subset/data/expect_lcar_0.ttx b/Tests/subset/data/expect_lcar_0.ttx
index feb866d7..0edcaa11 100644
--- a/Tests/subset/data/expect_lcar_0.ttx
+++ b/Tests/subset/data/expect_lcar_0.ttx
@@ -12,5 +12,5 @@
</Carets>
</LigatureCarets>
</lcar>
-
+
</ttFont>
diff --git a/Tests/subset/data/expect_lcar_1.ttx b/Tests/subset/data/expect_lcar_1.ttx
index 26b50082..72b4aabc 100644
--- a/Tests/subset/data/expect_lcar_1.ttx
+++ b/Tests/subset/data/expect_lcar_1.ttx
@@ -12,5 +12,5 @@
</Carets>
</LigatureCarets>
</lcar>
-
+
</ttFont>
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/data/expect_opbd_0.ttx b/Tests/subset/data/expect_opbd_0.ttx
index 55842a01..3f6c4199 100644
--- a/Tests/subset/data/expect_opbd_0.ttx
+++ b/Tests/subset/data/expect_opbd_0.ttx
@@ -14,5 +14,5 @@
</OpticalBoundsDeltas>
</OpticalBounds>
</opbd>
-
+
</ttFont>
diff --git a/Tests/subset/data/expect_opbd_1.ttx b/Tests/subset/data/expect_opbd_1.ttx
index 080abd91..7b40401b 100644
--- a/Tests/subset/data/expect_opbd_1.ttx
+++ b/Tests/subset/data/expect_opbd_1.ttx
@@ -14,5 +14,5 @@
</OpticalBoundsPoints>
</OpticalBounds>
</opbd>
-
+
</ttFont>
diff --git a/Tests/subset/data/expect_prop_0.ttx b/Tests/subset/data/expect_prop_0.ttx
index f8ca1509..64201e12 100644
--- a/Tests/subset/data/expect_prop_0.ttx
+++ b/Tests/subset/data/expect_prop_0.ttx
@@ -7,5 +7,5 @@
<DefaultProperties value="3"/>
</GlyphProperties>
</prop>
-
+
</ttFont>
diff --git a/Tests/subset/data/expect_prop_1.ttx b/Tests/subset/data/expect_prop_1.ttx
index f7f2d23c..a582ac21 100644
--- a/Tests/subset/data/expect_prop_1.ttx
+++ b/Tests/subset/data/expect_prop_1.ttx
@@ -10,5 +10,5 @@
</Properties>
</GlyphProperties>
</prop>
-
+
</ttFont>
diff --git a/Tests/subset/data/harfbuzz_repacker.ttx b/Tests/subset/data/harfbuzz_repacker.ttx
new file mode 100644
index 00000000..667d8687
--- /dev/null
+++ b/Tests/subset/data/harfbuzz_repacker.ttx
@@ -0,0 +1,1542 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="OTTO" ttLibVersion="4.29">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="cid12223"/>
+ <GlyphID id="2" name="cid12224"/>
+ <GlyphID id="3" name="cid12342"/>
+ <GlyphID id="4" name="cid15450"/>
+ <GlyphID id="5" name="cid15451"/>
+ <GlyphID id="6" name="cid15452"/>
+ <GlyphID id="7" name="cid18153"/>
+ <GlyphID id="8" name="cid18154"/>
+ <GlyphID id="9" name="cid18155"/>
+ <GlyphID id="10" name="cid59592"/>
+ <GlyphID id="11" name="cid59593"/>
+ <GlyphID id="12" name="cid61789"/>
+ <GlyphID id="13" name="cid61811"/>
+ <GlyphID id="14" name="cid62031"/>
+ <GlyphID id="15" name="cid62032"/>
+ <GlyphID id="16" name="cid62033"/>
+ <GlyphID id="17" name="cid62034"/>
+ <GlyphID id="18" name="cid62039"/>
+ <GlyphID id="19" name="cid62104"/>
+ <GlyphID id="20" name="cid62105"/>
+ <GlyphID id="21" name="cid62172"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.004"/>
+ <checkSumAdjustment value="0x96aa8849"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1000"/>
+ <created value="Mon Jun 15 05:06:56 2015"/>
+ <modified value="Sat Mar 5 04:13:07 2022"/>
+ <xMin value="30"/>
+ <yMin value="-82"/>
+ <xMax value="972"/>
+ <yMax value="838"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="3"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="1160"/>
+ <descent value="-320"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="1000"/>
+ <minLeftSideBearing value="30"/>
+ <minRightSideBearing value="28"/>
+ <xMaxExtent value="972"/>
+ <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>
+ <tableVersion value="0x5000"/>
+ <numGlyphs value="22"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="3"/>
+ <xAvgCharWidth value="979"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000000"/>
+ <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="325"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="2"/>
+ <bSerifStyle value="11"/>
+ <bWeight value="5"/>
+ <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="00001000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="GOOG"/>
+ <fsSelection value="00000000 01000000"/>
+ <usFirstCharIndex value="21417"/>
+ <usLastCharIndex value="24674"/>
+ <sTypoAscender value="880"/>
+ <sTypoDescender value="-120"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="1160"/>
+ <usWinDescent value="320"/>
+ <ulCodePageRange1 value="01100000 00101110 00000001 00000111"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="543"/>
+ <sCapHeight value="733"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="6"/>
+ </OS_2>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright © 2014, 2015 Adobe Systems Incorporated (http://www.adobe.com/).
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Noto Sans CJK JP Regular
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.004;GOOG;NotoSansCJKjp-Regular;ADOBE
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Noto Sans CJK JP Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.004;PS 1.004;hotconv 1.0.82;makeotf.lib2.5.63406
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ NotoSansCJKjp-Regular
+ </namerecord>
+ </name>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x53a9" name="cid12223"/><!-- CJK UNIFIED IDEOGRAPH-53A9 -->
+ <map code="0x53f1" name="cid12342"/><!-- CJK UNIFIED IDEOGRAPH-53F1 -->
+ <map code="0x5abe" name="cid15450"/><!-- CJK UNIFIED IDEOGRAPH-5ABE -->
+ <map code="0x6062" name="cid18153"/><!-- CJK UNIFIED IDEOGRAPH-6062 -->
+ </cmap_format_4>
+ <cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="64" language="0" nGroups="4">
+ <map code="0x53a9" name="cid12223"/><!-- CJK UNIFIED IDEOGRAPH-53A9 -->
+ <map code="0x53f1" name="cid12342"/><!-- CJK UNIFIED IDEOGRAPH-53F1 -->
+ <map code="0x5abe" name="cid15450"/><!-- CJK UNIFIED IDEOGRAPH-5ABE -->
+ <map code="0x6062" name="cid18153"/><!-- CJK UNIFIED IDEOGRAPH-6062 -->
+ </cmap_format_12>
+ <cmap_format_14 platformID="0" platEncID="5">
+ <map uv="0x53f1" uvs="0xe0100"/>
+ <map uv="0x5abe" uvs="0xe0100"/>
+ <map uv="0x53a9" uvs="0xe0100" name="cid61789"/>
+ <map uv="0x6062" uvs="0xe0100" name="cid61811"/>
+ <map uv="0x53a9" uvs="0xe0101" name="cid62031"/>
+ <map uv="0x53f1" uvs="0xe0101" name="cid62039"/>
+ <map uv="0x5abe" uvs="0xe0101" name="cid62104"/>
+ <map uv="0x6062" uvs="0xe0101" name="cid62172"/>
+ <map uv="0x6062" uvs="0xe0102"/>
+ <map uv="0x53a9" uvs="0xe0102" name="cid62032"/>
+ <map uv="0x5abe" uvs="0xe0102" name="cid62105"/>
+ <map uv="0x53a9" uvs="0xe0103" name="cid62033"/>
+ <map uv="0x53a9" uvs="0xe0104" name="cid62034"/>
+ <map uv="0x53a9" uvs="0xe0105"/>
+ </cmap_format_14>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x53a9" name="cid12223"/><!-- CJK UNIFIED IDEOGRAPH-53A9 -->
+ <map code="0x53f1" name="cid12342"/><!-- CJK UNIFIED IDEOGRAPH-53F1 -->
+ <map code="0x5abe" name="cid15450"/><!-- CJK UNIFIED IDEOGRAPH-5ABE -->
+ <map code="0x6062" name="cid18153"/><!-- CJK UNIFIED IDEOGRAPH-6062 -->
+ </cmap_format_4>
+ <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="64" language="0" nGroups="4">
+ <map code="0x53a9" name="cid12223"/><!-- CJK UNIFIED IDEOGRAPH-53A9 -->
+ <map code="0x53f1" name="cid12342"/><!-- CJK UNIFIED IDEOGRAPH-53F1 -->
+ <map code="0x5abe" name="cid15450"/><!-- CJK UNIFIED IDEOGRAPH-5ABE -->
+ <map code="0x6062" name="cid18153"/><!-- CJK UNIFIED IDEOGRAPH-6062 -->
+ </cmap_format_12>
+ </cmap>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-125"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+ <CFF>
+ <major value="1"/>
+ <minor value="0"/>
+ <CFFFont name="NotoSansCJKjp-Regular">
+ <ROS Registry="Adobe" Order="Identity" Supplement="0"/>
+ <Notice value="Copyright 2014, 2015 Adobe Systems Incorporated (http://www.adobe.com/). Noto is a trademark of Google Inc."/>
+ <FullName value="Noto Sans CJK JP Regular"/>
+ <FamilyName value="Noto Sans CJK JP"/>
+ <Weight value="Regular"/>
+ <isFixedPitch value="0"/>
+ <ItalicAngle value="0"/>
+ <UnderlinePosition value="-150"/>
+ <UnderlineThickness value="50"/>
+ <PaintType value="0"/>
+ <CharstringType value="2"/>
+ <FontMatrix value="0.001 0 0 0.001 0 0"/>
+ <FontBBox value="30 -82 972 838"/>
+ <StrokeWidth value="0"/>
+ <XUID value="1 11 9274312"/>
+ <CIDFontVersion value="1.004"/>
+ <CIDFontRevision value="0"/>
+ <CIDFontType value="0"/>
+ <CIDCount value="65535"/>
+ <!-- charset is dumped separately as the 'GlyphOrder' element -->
+ <FDSelect format="3"/>
+ <FDArray>
+ <FontDict index="0">
+ <FontName value="NotoSansCJKjp-Regular-Generic"/>
+ <Private>
+ <BlueValues value="-250 -250 1100 1100"/>
+ <BlueScale value="0.039625"/>
+ <BlueShift value="7"/>
+ <BlueFuzz value="1"/>
+ <StdHW value="40"/>
+ <StdVW value="40"/>
+ <StemSnapH value="40 120"/>
+ <StemSnapV value="40 120"/>
+ <ForceBold value="0"/>
+ <LanguageGroup value="1"/>
+ <ExpansionFactor value="0.06"/>
+ <initialRandomSeed value="0"/>
+ <defaultWidthX value="1000"/>
+ <nominalWidthX value="107"/>
+ </Private>
+ </FontDict>
+ <FontDict index="1">
+ <FontName value="NotoSansCJKjp-Regular-Ideographs"/>
+ <Private>
+ <BlueValues value="-250 -250 1100 1100"/>
+ <BlueScale value="0.039625"/>
+ <BlueShift value="7"/>
+ <BlueFuzz value="1"/>
+ <StdHW value="58"/>
+ <StdVW value="63"/>
+ <StemSnapH value="58 65 84"/>
+ <StemSnapV value="63 73 89"/>
+ <ForceBold value="0"/>
+ <LanguageGroup value="1"/>
+ <ExpansionFactor value="0.06"/>
+ <initialRandomSeed value="0"/>
+ <defaultWidthX value="1000"/>
+ <nominalWidthX value="0"/>
+ <Subrs>
+ <!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
+ <CharString index="0">
+ -351 rmoveto
+ -8 48 -27 80 -31 60 -101 callsubr
+ return
+ </CharString>
+ <CharString index="1">
+ 68 16 113 -95 callsubr
+ return
+ </CharString>
+ <CharString index="2">
+ 77 -94 callsubr
+ return
+ </CharString>
+ <CharString index="3">
+ 7 -100 callsubr
+ return
+ </CharString>
+ <CharString index="4">
+ hlineto
+ 14 70 12 68 11 61 -68 6 rcurveline
+ -10 -62 -12 -71 -14 -72 rrcurveto
+ -97 -67 84 -85 callsubr
+ return
+ </CharString>
+ <CharString index="5">
+ -59.5 return
+ </CharString>
+ <CharString index="6">
+ -46 -17 rcurveline
+ 28 -62 26 -81 8 -50 rrcurveto
+ return
+ </CharString>
+ <CharString index="7">
+ -92 callsubr
+ -34 return
+ </CharString>
+ <CharString index="8">
+ -103 callsubr
+ 11 44 30 return
+ </CharString>
+ <CharString index="9">
+ -96 callsubr
+ 141 -71 callsubr
+ return
+ </CharString>
+ <CharString index="10">
+ -98 callsubr
+ 190 -107 callsubr
+ return
+ </CharString>
+ <CharString index="11">
+ -106 callsubr
+ -60 rlinecurve
+ return
+ </CharString>
+ <CharString index="12">
+ 90 vvcurveto
+ -50 -104 callsubr
+ return
+ </CharString>
+ <CharString index="13">
+ -89 callsubr
+ 87 return
+ </CharString>
+ <CharString index="14">
+ hlineto
+ hintmask 011000000010101000000000
+ -62 181 vlineto
+ -31 -154 -66 -109 -129 -72 22 17 10 33 4 65 -18 5 -26 9 -14 11 rrcurveto
+ -80 -4 -4 -12 -27 hhcurveto
+ -113 hlineto
+ -33 -5 4 24 hvcurveto
+ 87 vlineto
+ 80 19 87 29 62 32 -54 46 rcurveline
+ -41 -26 -68 -26 -66 -20 rrcurveto
+ 98 -65 -240 vlineto
+ -68 19 -19 77 vhcurveto
+ -96 callgsubr
+ 18 15 2 7 11 hvcurveto
+ 14 -11 27 -24 10 -12 87 54 60 66 41 85 rrcurveto
+ hintmask 100000000000001010000000
+ -92 vlineto
+ -55 5 -16 15 -12 vhcurveto
+ -12 13 23 -4 18 hhcurveto
+ 53 hlineto
+ 18 21 3 6 11 hvcurveto
+ 14 7 9 13 5 18 5 17 3 52 1 44 -16 5 -20 10 -12 11 rrcurveto
+ -48 -2 -37 -2 -16 vhcurveto
+ -3 -15 -4 -9 -5 -3 rrcurveto
+ -3 -5 -12 -2 -9 hhcurveto
+ endchar
+ </CharString>
+ <CharString index="15">
+ -2 -87 -16 -108 return
+ </CharString>
+ <CharString index="16">
+ -86 callsubr
+ -23 31 -26 29 -76 callsubr
+ 72 45 58 63 45 86 37 -37 32 -36 22 -31 45 60 rcurveline
+ return
+ </CharString>
+ <CharString index="17">
+ 16 -78 callsubr
+ return
+ </CharString>
+ <CharString index="18">
+ vlineto
+ -220 vmoveto
+ return
+ </CharString>
+ <CharString index="19">
+ 70 912 -70 return
+ </CharString>
+ <CharString index="20">
+ 135 62 187 36 230 rrcurveto
+ 318 -129 rmoveto
+ -16 -85 -28 -89 -34 -60 15 -5 27 -13 11 -9 34 63 31 95 19 90 rrcurveto
+ -337 5 rmoveto
+ -12 -84 -21 -85 -36 -58 15 -7 24 -15 10 -9 36 62 26 93 15 93 rrcurveto
+ 58 112 rmoveto
+ -13 -323 -42 -198 -222 -79 14 -15 19 -26 9 -20 128 51 72 83 42 122 42 -113 66 -88 92 -48 11 18 20 25 16 13 -112 49 -72 112 -36 137 17 84 8 99 4 114 rrcurveto
+ endchar
+ </CharString>
+ <CharString index="21">
+ -100 callgsubr
+ -120 -99 callsubr
+ return
+ </CharString>
+ <CharString index="22">
+ hlineto
+ -24 -124 -27 -121 -22 -85 59 -35 rcurveline
+ return
+ </CharString>
+ <CharString index="23">
+ 189 721 rmoveto
+ 755 68 -825 -294 hlineto
+ -156 -7 -220 -76 -157 vhcurveto
+ 18 -7 31 -18 14 -11 rrcurveto
+ 79 164 11 241 164 vvcurveto
+ return
+ </CharString>
+ <CharString index="24">
+ 104 54 73 69 50 91 rrcurveto
+ -117 vlineto
+ -45 4 -14 16 -13 vhcurveto
+ -13 14 23 -3 22 hhcurveto
+ 55 hlineto
+ 18 22 3 7 12 hvcurveto
+ 15 8 10 12 6 18 6 17 3 50 1 45 -17 6 -24 13 -12 12 1 -46 -2 -40 -2 -15 -3 -11 -6 -7 -5 -4 rrcurveto
+ -3 -6 -12 -1 -10 hhcurveto
+ endchar
+ </CharString>
+ <CharString index="25">
+ 284 -154 rmoveto
+ -70 -166 70 vlineto
+ -193 vmoveto
+ 71 166 -71 vlineto
+ 70 -316 rmoveto
+ 56 -236 75 230 54 -230 75 227 305 -140 vlineto
+ 10 22 11 26 11 24 -69 16 rcurveline
+ -5 -25 -10 -35 -10 -28 rrcurveto
+ -89 -678 64 -92 callgsubr
+ return
+ </CharString>
+ <CharString index="26">
+ rmoveto
+ 69 913 -69 hlineto
+ return
+ </CharString>
+ <CharString index="27">
+ 61 -31 69 75 53 cntrmask 0110111011111000
+ cntrmask 1001000110100101
+ hintmask 1110110111111101
+ 949 715 rmoveto
+ 68 -827 -281 vlineto
+ -154 -7 -218 -77 -155 vhcurveto
+ 18 -7 30 -18 14 -11 rrcurveto
+ 81 163 10 238 162 vvcurveto
+ 213 vlineto
+ return
+ </CharString>
+ <CharString index="28">
+ 6 -99 callgsubr
+ return
+ </CharString>
+ <CharString index="29">
+ -15 19 -26 10 -18 return
+ </CharString>
+ <CharString index="30">
+ -164 68 164 345 -85 hlineto
+ -13 -69 callsubr
+ 9 -18 9 -25 3 -18 -49 callsubr
+ 26 -107 callgsubr
+ 85 56 vlineto
+ endchar
+ </CharString>
+ <CharString index="31">
+ -27 rlinecurve
+ -43 -85 -56 -59 callsubr
+ return
+ </CharString>
+ <CharString index="32">
+ rmoveto
+ -88 callsubr
+ hlineto
+ return
+ </CharString>
+ <CharString index="33">
+ -14 -1 vhcurveto
+ -13 -1 -48 0 return
+ </CharString>
+ <CharString index="34">
+ -147 69 114 55 -114 60 134 57 -134 82 -68 -82 -154 82 -67 -82 -129 -57 129 -60 -105 -55 105 -69 -144 -56 250 -57 hlineto
+ return
+ </CharString>
+ <CharString index="35">
+ -55 9 -4 -83 -18 -109 -27 -64 rlinecurve
+ 126 -467 -81 callsubr
+ 193 -350 rmoveto
+ -8 48 -27 79 -29 62 -53 -20 rcurveline
+ 28 -63 return
+ </CharString>
+ <CharString index="36">
+ -465 -75 callsubr
+ return
+ </CharString>
+ <CharString index="37">
+ hstemhm
+ 83 50 39 70 return
+ </CharString>
+ <CharString index="38">
+ -60 callsubr
+ -52 2 return
+ </CharString>
+ <CharString index="39">
+ -61.5 return
+ </CharString>
+ <CharString index="40">
+ -38 -90 callsubr
+ return
+ </CharString>
+ <CharString index="41">
+ 68 -102 callsubr
+ return
+ </CharString>
+ <CharString index="42">
+ hstem
+ 83 50 39 70 return
+ </CharString>
+ <CharString index="43">
+ 232 -58 callsubr
+ 762 return
+ </CharString>
+ <CharString index="44">
+ -134 hlineto
+ 134 return
+ </CharString>
+ <CharString index="45">
+ rrcurveto
+ hintmask 101000000000001000000000
+ 85 -380 rmoveto
+ -9 -7 2 4 -4 hvcurveto
+ -5 3 -1 11 19 vvcurveto
+ 275 -6 4 return
+ </CharString>
+ <CharString index="46">
+ 10 18 20 25 16 return
+ </CharString>
+ <CharString index="47">
+ -4 -54 callsubr
+ return
+ </CharString>
+ <CharString index="48">
+ -62 -67 -67 callsubr
+ return
+ </CharString>
+ <CharString index="49">
+ -57 callsubr
+ -50 callsubr
+ return
+ </CharString>
+ <CharString index="50">
+ -53 callsubr
+ -90 -34 29 -35 28 return
+ </CharString>
+ <CharString index="51">
+ rmoveto
+ -11 -131 -23 -110 return
+ </CharString>
+ <CharString index="52">
+ -124 hlineto
+ 124 return
+ </CharString>
+ <CharString index="53">
+ -3 -74 callsubr
+ return
+ </CharString>
+ <CharString index="54">
+ -56 callsubr
+ -33 return
+ </CharString>
+ <CharString index="55">
+ -40 -79 callsubr
+ return
+ </CharString>
+ <CharString index="56">
+ 120 -69 -120 return
+ </CharString>
+ <CharString index="57">
+ -34 25 18 76 18 87 18 86 rrcurveto
+ return
+ </CharString>
+ <CharString index="58">
+ rrcurveto
+ 72 45 0 11 28 hvcurveto
+ return
+ </CharString>
+ <CharString index="59">
+ hvcurveto
+ 25 11 7 19 36 vvcurveto
+ return
+ </CharString>
+ <CharString index="60">
+ -7 31 -18 13 -11 rrcurveto
+ return
+ </CharString>
+ <CharString index="61">
+ -14 19 -25 9 -17 return
+ </CharString>
+ </Subrs>
+ </Private>
+ </FontDict>
+ </FDArray>
+ <CharStrings>
+ <CharString name=".notdef" fdSelectIndex="0">
+ endchar
+ </CharString>
+ <CharString name="cid12223" fdSelectIndex="1">
+ -67 64 55 56 75 54 74 61 -60 56 71 52 70 56 -46 61 83 68 hstemhm
+ 122 69 54 64 166 61 61 61 80 -80 callsubr
+ hintmask 0110111000110000
+ -82 callsubr
+ hintmask 0001000100001010
+ 349 320 rmoveto
+ 199 -95 callgsubr
+ -4 -73 -6 -66 -11 -60 rrcurveto
+ hintmask 1001000000000100
+ 82 -375 rmoveto
+ -10 -7 1 4 -5 hvcurveto
+ -6 3 -2 6 8 vvcurveto
+ 281 vlineto
+ 3 11 rlineto
+ 144 61 hlineto
+ hintmask 0001000100001010
+ -86 callgsubr
+ 11 60 7 66 5 73 rrcurveto
+ 99 61 -339 -260 -40 -61 177 hlineto
+ -41 -161 -80 -107 -154 -69 16 -12 27 -27 9 -11 rrcurveto
+ hintmask 1000000000001101
+ -83 callsubr
+ </CharString>
+ <CharString name="cid12224" fdSelectIndex="1">
+ -58 57 292 65 -54 59 88 56 75 61 80 68 hstemhm
+ 119 71 55 66 155 64 86 63 58 68 -43 66 81 53 cntrmask 1100111100101000
+ cntrmask 0011100110000000
+ hintmask 1011111111101000
+ 946 721 rmoveto
+ 68 -827 -333 vlineto
+ -149 -6 -202 -73 -143 vhcurveto
+ 18 -7 32 -18 13 -13 rrcurveto
+ 76 151 11 225 156 vvcurveto
+ 265 vlineto
+ 276 -139 rmoveto
+ -77 -155 77 vlineto
+ -221 vmoveto
+ 88 155 -88 vlineto
+ 79 -307 rmoveto
+ -16 54 -46 86 -43 64 -53 -21 rcurveline
+ 17 -28 18 -31 16 -30 -127 -47 rcurveline
+ 201 219 340 -285 -514 vlineto
+ -41 -22 -22 -16 -9 vhcurveto
+ 12 -15 16 -28 6 -18 rrcurveto
+ -1 vlineto
+ 17 15 27 13 178 71 11 -24 9 -21 5 -19 rrcurveto
+ 343 -30 rmoveto
+ -8 -7 1 4 -5 hvcurveto
+ -5 3 -1 10 18 vvcurveto
+ hintmask 1101111111101000
+ 220 -34 vlineto
+ 3 12 4 12 4 12 rrcurveto
+ 154 65 hlineto
+ hintmask 1101111111011000
+ -139 hlineto
+ 17 77 9 77 5 70 rrcurveto
+ 104 61 -341 -61 -83 callgsubr
+ -5 -68 -8 -79 -18 -77 rrcurveto
+ -91 hlineto
+ 14 58 16 85 9 39 rrcurveto
+ -63 hlineto
+ -7 -39 -22 -117 -9 -18 -6 -16 -8 -5 -12 -5 7 -13 15 -30 5 -16 rrcurveto
+ -1 vlineto
+ 8 8 29 5 37 hhcurveto
+ 60 hlineto
+ -38 -122 -70 -115 -131 -72 17 -12 20 -21 10 -15 rrcurveto
+ hintmask 1011111111101000
+ 104 59 67 84 45 94 rrcurveto
+ -130 vlineto
+ -55 6 -16 15 -11 vhcurveto
+ -12 14 24 -5 20 hhcurveto
+ 55 hlineto
+ 16 23 3 6 12 hvcurveto
+ 14 7 9 11 6 17 6 17 3 47 2 41 -18 6 -22 10 -13 11 rrcurveto
+ -43 -1 -33 -4 -16 vhcurveto
+ -2 -12 -4 -8 -6 -3 rrcurveto
+ -3 -5 -10 -1 -10 hhcurveto
+ endchar
+ </CharString>
+ <CharString name="cid12342" fdSelectIndex="1">
+ -59 73 170 70 411 69 hstem
+ 77 72 144 73 189 76 255 72 cntrmask 01111100
+ 293 665 rmoveto
+ -411 -144 411 vlineto
+ 217 69 rmoveto
+ -289 -646 72 96 217 hlineto
+ 316 -170 rmoveto
+ -43 -8 10 50 hvcurveto
+ 287 vlineto
+ 112 75 108 90 83 96 -72 49 rcurveline
+ -61 -77 -82 -75 -88 -65 rrcurveto
+ 361 -76 -414 vlineto
+ -60 -39 -60 -35 -58 -27 17 -15 24 -26 12 -16 41 20 42 23 42 26 rrcurveto
+ -236 vlineto
+ -105 27 -30 91 vhcurveto
+ -97 callgsubr
+ 93 20 60 167 10 hvcurveto
+ -22 5 -30 14 -20 16 rrcurveto
+ -150 -5 -7 -39 -43 hhcurveto
+ endchar
+ </CharString>
+ <CharString name="cid15450" fdSelectIndex="1">
+ -75 62 102 61 74 52 71 54 60 55 48 67 -45 53 64 55 hstemhm
+ 311 71 57 68 17 66 41 69 44 67 19 69 cntrmask 1111101101010100
+ cntrmask 0000000000101000
+ hintmask 1111101101010100
+ 700 150 rmoveto
+ 74 130 -74 vlineto
+ -323 74 rmoveto
+ 124 -74 -55 callsubr
+ 197 rmoveto
+ -71 -124 71 vlineto
+ 323 hmoveto
+ -71 -130 71 vlineto
+ hintmask 0000010010000000
+ -389 217 -58 callsubr
+ hintmask 0000101100101000
+ 395 22 rmoveto
+ 154 -70 -154 hlineto
+ 187 vmoveto
+ 154 -64 -154 hlineto
+ hintmask 0101100000010100
+ 367 -489 rmoveto
+ -58 251 -199 60 252 55 hlineto
+ hintmask 0001101100111000
+ -141 70 103 53 -103 64 122 55 -122 78 -67 -78 -154 78 -66 -78 -125 -55 125 -64 -98 -53 98 -70 -138 -55 245 -60 hlineto
+ hintmask 1101010011000100
+ -192 -251 -65 -40 hlineto
+ -21 24 -26 25 -28 25 45 113 27 145 11 185 -43 6 rcurveline
+ -91 callsubr
+ -4 5 -4 6 -5 5 rrcurveto
+ 47 -164 68 164 323 -87 hlineto
+ -11 -4 -3 -11 -1 vhcurveto
+ -13 -1 -40 0 -46 1 8 -17 11 -27 3 -18 rrcurveto
+ 62 41 0 11 26 -48 callsubr
+ 87 58 vlineto
+ endchar
+ </CharString>
+ <CharString name="cid15451" fdSelectIndex="1">
+ -76 61 101 59 74 52 72 56 57 56 51 68 -50 55 60 57 hstemhm
+ 160 67 55 69 66 68 28 67 39 66 49 68 28 69 cntrmask 1111101100101010
+ cntrmask 0000000000010100
+ hintmask 1111101100101010
+ 685 145 rmoveto
+ 74 145 -74 vlineto
+ -345 74 rmoveto
+ 134 -74 -63 callsubr
+ 198 rmoveto
+ -72 -134 72 vlineto
+ 345 hmoveto
+ -72 -145 72 vlineto
+ hintmask 0000010001000000
+ -403 220 rmoveto
+ -10 -120 -18 -101 -29 -86 -29 24 -31 22 -29 21 17 69 18 84 16 87 rrcurveto
+ hintmask 0000101100010100
+ 393 18 rmoveto
+ 154 -69 -154 hlineto
+ 184 vmoveto
+ 154 -60 -154 hlineto
+ hintmask 0101100000001010
+ 375 -491 rmoveto
+ -56 254 -214 57 264 56 hlineto
+ hintmask 0001101100011100
+ -73 callsubr
+ hintmask 1101010011100010
+ -202 -254 -51 -17 hlineto
+ -24 27 -29 28 -34 29 39 107 24 137 9 171 -52 callsubr
+ -100 hlineto
+ 12 68 10 67 6 61 -67 3 rcurveline
+ -6 -61 -9 -68 -11 -70 rrcurveto
+ -90 -68 78 hlineto
+ -19 -102 -22 -98 -21 -71 45 -31 48 -38 44 -38 -42 -93 -56 -66 -69 -40 14 -46 callsubr
+ 73 48 60 67 44 93 38 -36 31 -36 22 -30 47 50 rcurveline
+ 30 -77 callsubr
+ </CharString>
+ <CharString name="cid15452" fdSelectIndex="1">
+ -76 61 101 59 74 52 72 56 57 56 -103 callgsubr
+ 70 -68 callsubr
+ -66 callsubr
+ 66 -102 callgsubr
+ 55 60 57 hstemhm
+ 43 139 -45 68 57 66 41 17 31 68 28 67 39 66 49 68 28 69 cntrmask 111110001100110101000000
+ cntrmask 000000000000001010100000
+ hintmask 000000100000100000000000
+ 262 556 rmoveto
+ -1 -96 -8 -114 -39 -111 -27 25 -29 25 -27 24 14 70 15 84 14 87 rrcurveto
+ -105 callgsubr
+ 211 13 rmoveto
+ hintmask 000000001000110000000000
+ 1 66 -58 -3 rlineto
+ hintmask 000000100010100000000000
+ 69 -66 -72 vlineto
+ -80 -4 rlineto
+ -106 callgsubr
+ 10 74 8 72 5 65 -68 3 rcurveline
+ -4 -66 -8 -76 -10 -75 rrcurveto
+ hintmask 000001000010100000000000
+ -76 -3 8 -70 60 3 -15 -97 -17 -93 -16 -68 rlinecurve
+ 41 -35 45 -42 41 -42 -32 -70 -47 -68 -68 -61 17 -10 24 -22 11 -15 65 59 47 66 34 68 29 -32 26 -29 17 -24 44 55 rcurveline
+ -20 28 -31 33 -35 35 rrcurveto
+ hintmask 011100010000101010100000
+ 51 132 9 135 1 111 rrcurveto
+ 357 -415 rmoveto
+ 74 145 -74 vlineto
+ -345 74 rmoveto
+ 134 -74 -63 callsubr
+ 198 rmoveto
+ -72 -134 72 vlineto
+ 345 hmoveto
+ -72 -145 72 vlineto
+ hintmask 000010001100000101000000
+ -105 238 rmoveto
+ 154 -69 -154 hlineto
+ 184 vmoveto
+ 154 -60 -154 hlineto
+ hintmask 010110000000000010100000
+ 375 -491 rmoveto
+ -56 254 -214 57 264 56 hlineto
+ hintmask 000110001100010111000000
+ -73 callsubr
+ hintmask 110100000000001000100000
+ -202 -254 -51 -59 51 -77 callsubr
+ </CharString>
+ <CharString name="cid18153" fdSelectIndex="1">
+ 656 68 -65 callsubr
+ 161 66 60 55 87 68 cntrmask 01110100
+ 81 367 rmoveto
+ 36 -97 callsubr
+ 157 192 rmoveto
+ 486 68 -486 112 -66 -112 -82 -68 82 -109 hlineto
+ -191 -12 -213 -118 -175 vhcurveto
+ 17 -9 26 -21 13 -13 rrcurveto
+ 127 187 13 228 208 vvcurveto
+ 382 -272 rmoveto
+ 32 56 37 93 31 77 -65 16 rcurveline
+ -19 -69 -39 -99 -30 -60 rrcurveto
+ -274 -33 rmoveto
+ 40 57 12 94 8 84 -55 13 rcurveline
+ -5 -81 -15 -88 -34 -51 rrcurveto
+ 264 344 rmoveto
+ -68 -259 hlineto
+ -71 -36 -210 -209 -108 vhcurveto
+ 13 -15 19 -28 9 -15 165 88 61 159 12 71 12 -70 55 -164 154 -84 10 17 20 29 12 15 rrcurveto
+ -195 105 -34 212 69 vvcurveto
+ endchar
+ </CharString>
+ <CharString name="cid18154" fdSelectIndex="1">
+ 617 68 hstemhm
+ 92 55 22 69 266 70 128 63 hintmask 11000000
+ 99 370 rmoveto
+ 28 72 17 113 3 83 rrcurveto
+ hintmask 10111000
+ -72 callsubr
+ 26 -81 7 -50 rrcurveto
+ 635 -89 callgsubr
+ 68 -384 vlineto
+ 7 47 5 49 5 51 -70 6 rcurveline
+ -5 -53 -5 -51 -7 -49 rrcurveto
+ -139 -68 130 hlineto
+ -33 -210 -57 -173 -109 -118 16 -12 28 -26 11 -13 115 -87 callsubr
+ </CharString>
+ <CharString name="cid18155" fdSelectIndex="1">
+ 617 68 hstemhm
+ 92 55 22 69 240 70 -54 70 -8 60 76 66 cntrmask 00100010
+ hintmask 11000000
+ 99 370 rmoveto
+ 28 72 17 113 3 83 rrcurveto
+ hintmask 10110000
+ -72 callsubr
+ 25 -81 8 -50 rrcurveto
+ 635 -89 callgsubr
+ 68 -393 vlineto
+ hintmask 10001000
+ 6 47 5 49 5 51 -70 6 rcurveline
+ hintmask 10010000
+ -4 -53 -6 -51 -6 -49 rrcurveto
+ -131 -68 -84 callgsubr
+ -32 -213 -56 -176 -109 -120 16 -11 29 -26 10 -14 116 137 60 191 35 232 rrcurveto
+ 332 -129 rmoveto
+ -19 -82 -32 -90 -33 -62 16 -5 26 -13 13 -9 31 64 35 97 22 85 rrcurveto
+ hintmask 10000110
+ -313 -229 rmoveto
+ -4 59 -16 107 -16 84 -56 -9 rcurveline
+ 14 -83 14 -109 4 -64 rrcurveto
+ 136 355 rmoveto
+ -6 -328 -13 -180 -251 -94 14 -13 20 -25 9 -17 138 55 71 80 39 115 42 -112 66 -89 93 -48 -61 callsubr
+ 13 -113 50 -73 115 -36 139 14 84 4 98 2 114 rrcurveto
+ endchar
+ </CharString>
+ <CharString name="cid59592" fdSelectIndex="1">
+ -49 71 205 69 377 70 hstem
+ 79 70 159 71 161 75 281 69 cntrmask 01111100
+ 308 673 rmoveto
+ -377 -159 377 vlineto
+ 230 70 rmoveto
+ -300 -602 70 86 230 hlineto
+ 292 -205 rmoveto
+ -47 -9 10 51 hvcurveto
+ 345 vlineto
+ 345 85 -18 68 -327 -80 rlineto
+ 319 -75 -337 vlineto
+ -158 -39 17 -69 141 35 rlineto
+ -326 vlineto
+ -105 29 -28 95 vhcurveto
+ 178 hlineto
+ 92 21 53 154 10 hvcurveto
+ -21 6 -29 13 -19 13 rrcurveto
+ -134 -6 -7 -34 -45 hhcurveto
+ endchar
+ </CharString>
+ <CharString name="cid59593" fdSelectIndex="1">
+ -50 72 192 70 219 73 97 70 hstem
+ 78 71 159 72 160 75 254 70 cntrmask 10101110
+ cntrmask 01010000
+ 308 673 rmoveto
+ -389 -159 389 vlineto
+ 231 70 rmoveto
+ -302 -626 71 97 231 hlineto
+ 576 289 rmoveto
+ 73 -341 244 -75 -244 -116 -73 116 -421 vlineto
+ -104 27 -28 90 vhcurveto
+ -90 callgsubr
+ 89 20 53 154 10 hvcurveto
+ -22 5 -29 14 -19 14 rrcurveto
+ -134 -6 -6 -34 -41 hhcurveto
+ -85 callgsubr
+ -42 -9 10 48 hvcurveto
+ 423 vlineto
+ endchar
+ </CharString>
+ <CharString name="cid61789" fdSelectIndex="1">
+ -65 59 305 63 87 56 74 63 79 68 hstemhm
+ 119 70 59 65 157 64 59 63 82 68 -33 69 71 52 cntrmask 1111111110000000
+ cntrmask 0000001001010000
+ -87 callgsubr
+ 189 721 rmoveto
+ 755 68 -825 -294 hlineto
+ -157 -7 -219 -81 -157 vhcurveto
+ 18 -47 callsubr
+ 85 164 11 240 165 vvcurveto
+ 281 87 rmoveto
+ -77 -157 -105 callsubr
+ 157 -87 vlineto
+ 97 -316 rmoveto
+ -15 58 -45 88 -46 65 -59 -20 rcurveline
+ 19 -27 19 -32 16 -31 -49 -18 -49 -17 -45 -15 rrcurveto
+ 205 221 340 -286 -567 vlineto
+ -23 -8 -21 -7 -19 -6 24 -67 rcurveline
+ 77 30 99 37 96 38 10 -25 9 -24 5 -20 rrcurveto
+ 151 339 rmoveto
+ 217 vlineto
+ hintmask 0101000000100000
+ -94 callgsubr
+ -3 -75 -5 -73 -13 -69 rrcurveto
+ hintmask 1100000001000000
+ 81 -368 rmoveto
+ -9 -6 1 3 -5 hvcurveto
+ -5 4 -2 11 19 vvcurveto
+ 264 vlineto
+ 1 3 rlineto
+ 144 63 hlineto
+ hintmask 0101000010100000
+ -131 hlineto
+ 12 69 6 74 4 74 rrcurveto
+ 92 63 -341 -280 -38 -63 182 hlineto
+ -39 -140 -82 -117 -184 -68 15 -13 20 -25 8 -16 rrcurveto
+ hintmask 1000000011010000
+ 127 49 82 73 54 87 rrcurveto
+ -92 vlineto
+ -56 6 -16 15 -13 vhcurveto
+ -12 14 25 -5 20 hhcurveto
+ 56 hlineto
+ 17 23 3 6 11 hvcurveto
+ 15 7 10 12 6 18 rrcurveto
+ 6 15 3 48 41 vvcurveto
+ -16 6 -23 11 -13 12 rrcurveto
+ -43 -2 -34 -2 -14 vhcurveto
+ -2 -14 -5 -8 -6 -3 rrcurveto
+ -3 -5 -12 -1 -10 hhcurveto
+ endchar
+ </CharString>
+ <CharString name="cid61811" fdSelectIndex="1">
+ -75 40 751 67 -65 callsubr
+ 133 69 77 58 86 72 cntrmask 00111010
+ 81 367 rmoveto
+ 36 -97 callsubr
+ 132 252 rmoveto
+ 499 67 -568 -300 hlineto
+ -156 -10 -212 -102 -150 vhcurveto
+ 17 -7 32 -18 13 -11 rrcurveto
+ 104 154 15 236 164 vvcurveto
+ 403 -190 rmoveto
+ 31 57 38 92 30 78 -66 15 rcurveline
+ -19 -68 -39 -99 -30 -60 rrcurveto
+ -275 -36 rmoveto
+ 40 56 14 95 8 85 -58 14 rcurveline
+ -7 -81 -16 -87 -35 -52 rrcurveto
+ 274 352 rmoveto
+ -72 -268 hlineto
+ -74 -38 -216 -214 -112 vhcurveto
+ 13 -15 21 -30 9 -16 169 90 63 165 12 73 13 -72 56 -169 158 -87 11 18 21 31 13 15 rrcurveto
+ -200 110 -35 217 72 vvcurveto
+ endchar
+ </CharString>
+ <CharString name="cid62031" fdSelectIndex="1">
+ -65 53 -29 60 287 62 -58 51 78 49 78 51 -45 62 82 63 hstemhm
+ 122 70 48 61 -58 65 165 62 -41 58 53 61 83 63 -43 65 81 48 cntrmask 010111011101010000000000
+ cntrmask 101000110010001010000000
+ hintmask 010110111101011010000000
+ 949 716 rmoveto
+ 63 -827 -297 vlineto
+ -153 -7 -214 -77 -152 vhcurveto
+ 18 -7 31 -19 14 -11 rrcurveto
+ 80 159 11 236 161 vvcurveto
+ 234 vlineto
+ 281 -228 rmoveto
+ -172 hlineto
+ hintmask 000111000101010000000000
+ 78 172 vlineto
+ -205 vmoveto
+ -172 78 172 hlineto
+ 62 178 rmoveto
+ -128 hlineto
+ 10 22 11 23 10 24 -69 16 rcurveline
+ -6 -24 -11 -32 -11 -29 rrcurveto
+ -101 -307 295 hlineto
+ 131 58 rmoveto
+ hintmask 000100100000010100000000
+ 204 103 vlineto
+ -3 -75 -4 -68 -9 -61 -62 callsubr
+ 140 vlineto
+ hintmask 000100100000010100000000
+ 62 -129 vlineto
+ 9 62 5 67 3 75 rrcurveto
+ 104 62 -333 -266 -44 -93 callsubr
+ </CharString>
+ <CharString name="cid62032" fdSelectIndex="1">
+ -67 64 55 56 75 54 70 61 -56 56 71 52 70 56 -47 60 85 68 hstemhm
+ 122 69 54 64 166 61 66 61 75 -80 callsubr
+ hintmask 1110111011111101
+ -82 callsubr
+ 520 -55 rmoveto
+ -10 -7 1 4 -5 hvcurveto
+ -6 3 -2 6 8 vvcurveto
+ hintmask 1111010111111101
+ 281 vlineto
+ 2 7 rlineto
+ 148 61 hlineto
+ hintmask 1111010111111011
+ -91 callgsubr
+ 11 61 8 67 5 74 rrcurveto
+ 101 60 -365 -60 -88 callgsubr
+ -4 -74 -6 -68 -12 -60 rrcurveto
+ -83 167 -61 hlineto
+ hintmask 1110110111111101
+ -167 -44 vlineto
+ hintmask 1111010111111101
+ -61 175 vlineto
+ -41 -159 -80 -106 -153 -68 16 -12 27 -27 9 -11 -83 callsubr
+ </CharString>
+ <CharString name="cid62033" fdSelectIndex="1">
+ -65 58 304 63 -59 59 88 55 76 63 79 68 hstemhm
+ 119 70 58 65 158 63 83 58 63 68 -32 68 71 53 cntrmask 1100111111000000
+ cntrmask 0011100100101000
+ hintmask 1101111111100000
+ -84 callsubr
+ 281 87 rmoveto
+ -79 -158 79 vlineto
+ hintmask 0011100110000000
+ -221 vmoveto
+ 87 158 -87 vlineto
+ 98 -316 rmoveto
+ -15 59 -44 88 -46 65 -59 -20 rcurveline
+ 19 -27 18 -32 16 -31 -50 -17 -49 -17 -46 -16 rrcurveto
+ 204 221 341 -286 -567 vlineto
+ -23 -7 -21 -7 -19 -6 26 -67 rcurveline
+ 78 29 99 39 95 37 11 -26 8 -24 5 -20 rrcurveto
+ hintmask 0100100001010000
+ 168 337 rmoveto
+ 219 99 vlineto
+ -4 -75 -5 -74 -13 -70 rrcurveto
+ hintmask 1100000000100000
+ 81 -367 rmoveto
+ -9 -6 1 3 -5 hvcurveto
+ -6 4 -1 11 18 vvcurveto
+ 264 -1 vlineto
+ 1 3 rlineto
+ 145 63 hlineto
+ hintmask 0100100001010000
+ -86 callgsubr
+ 13 70 6 74 4 75 rrcurveto
+ 96 63 -371 -63 50 -219 -62 -63 182 hlineto
+ -40 -139 -83 -118 -182 -68 15 -13 19 -24 9 -17 rrcurveto
+ hintmask 1000000000101000
+ 127 50 82 74 54 88 rrcurveto
+ -94 vlineto
+ -56 5 -16 16 -13 vhcurveto
+ -12 14 25 -4 20 hhcurveto
+ 56 hlineto
+ 17 23 2 6 11 hvcurveto
+ 15 7 10 12 6 17 6 16 3 48 1 41 -18 5 -23 12 -12 12 rrcurveto
+ -44 -2 -33 -2 -15 vhcurveto
+ -2 -13 -5 -8 -6 -3 rrcurveto
+ -3 -5 -12 -1 -10 hhcurveto
+ endchar
+ </CharString>
+ <CharString name="cid62034" fdSelectIndex="1">
+ -65 53 -29 60 287 62 -58 51 78 49 78 51 -43 60 82 63 hstemhm
+ 122 70 48 61 -58 65 165 62 -41 58 59 59 79 63 -43 66 80 48 cntrmask 010111011101010000000000
+ cntrmask 101000110010001010000000
+ hintmask 010110111101011010000000
+ 949 716 rmoveto
+ 63 -827 -297 vlineto
+ -153 -7 -214 -77 -152 vhcurveto
+ 18 -7 31 -19 14 -11 rrcurveto
+ 80 159 11 236 161 vvcurveto
+ 234 vlineto
+ 281 -228 rmoveto
+ -172 hlineto
+ hintmask 000111000101010000000000
+ 78 172 vlineto
+ -205 vmoveto
+ -172 78 172 hlineto
+ 62 178 rmoveto
+ -128 hlineto
+ 10 22 11 23 10 24 -69 16 rcurveline
+ -6 -24 -11 -32 -11 -29 rrcurveto
+ -101 -307 295 hlineto
+ 135 58 rmoveto
+ hintmask 000100100000010100000000
+ 206 99 vlineto
+ -3 -76 -4 -68 -9 -62 -62 callsubr
+ 141 vlineto
+ hintmask 000100100000010100000000
+ 62 -130 vlineto
+ 9 62 5 69 4 75 rrcurveto
+ 100 60 -367 -60 43 -206 -50 -93 callsubr
+ </CharString>
+ <CharString name="cid62039" fdSelectIndex="1">
+ -52 71 206 69 377 69 hstem
+ 79 70 162 72 100 75 331 70 cntrmask 01111100
+ 311 671 rmoveto
+ -377 -162 377 vlineto
+ 234 69 rmoveto
+ -304 -618 70 103 234 hlineto
+ 240 -206 rmoveto
+ -54 -11 12 55 hvcurveto
+ 261 vlineto
+ 131 76 143 98 93 94 -55 48 rcurveline
+ -74 -77 -120 -88 -118 -70 rrcurveto
+ 407 -75 -748 vlineto
+ -109 32 -30 103 vhcurveto
+ 206 hlineto
+ 102 22 56 157 11 hvcurveto
+ -21 5 -30 13 -19 14 rrcurveto
+ -138 -7 -9 -36 -53 hhcurveto
+ endchar
+ </CharString>
+ <CharString name="cid62104" fdSelectIndex="1">
+ -76 61 94 56 74 54 69 54 80 56 42 67 -43 53 64 55 hstemhm
+ 311 71 61 68 13 66 41 69 44 67 15 69 cntrmask 1111101101010100
+ cntrmask 0000000000101000
+ hintmask 0110101100111000
+ 590 588 rmoveto
+ 154 -66 -154 hlineto
+ 183 vmoveto
+ 154 -64 -154 hlineto
+ 362 -119 rmoveto
+ -141 66 103 53 -103 64 122 55 -122 76 -67 -76 -154 76 -66 -76 -123 -55 123 -64 -98 -53 98 -66 -138 -56 566 hlineto
+ -252 -331 rmoveto
+ 74 vlineto
+ hintmask 1111010011010100
+ 126 -74 hlineto
+ -315 74 rmoveto
+ 120 -74 -120 hlineto
+ 197 vmoveto
+ -51 callsubr
+ hlineto
+ 315 69 rmoveto
+ -69 -126 69 vlineto
+ -389 -64 callsubr
+ -429 rmoveto
+ -62 74 52 54 -52 123 -195 59 -69 -59 -188 -123 -66 -54 66 -74 -71 -23 hlineto
+ -21 23 -25 25 -27 24 45 113 27 145 11 185 -43 6 rcurveline
+ -91 callsubr
+ -5 6 rlineto
+ 43 -155 68 155 315 -79 hlineto
+ -11 -4 -4 -11 vhcurveto
+ -13 -1 -40 0 -46 2 9 -17 10 -27 3 -18 rrcurveto
+ 62 41 1 10 26 hvcurveto
+ 25 11 7 19 35 vvcurveto
+ 79 -101 callgsubr
+ </CharString>
+ <CharString name="cid62105" fdSelectIndex="1">
+ -76 61 94 58 74 52 69 54 80 54 44 67 -45 53 65 54 hstemhm
+ 311 71 57 68 17 66 43 65 46 67 19 69 cntrmask 1111101101010100
+ cntrmask 0000000000101000
+ hintmask 0110101100111000
+ 590 586 rmoveto
+ 154 -66 -154 hlineto
+ 184 vmoveto
+ 154 -65 -154 hlineto
+ 362 -119 rmoveto
+ -141 66 104 53 -104 65 122 54 -122 78 -67 -78 -154 78 -66 -78 -123 -54 123 -65 -98 -53 98 -66 -138 -54 566 hlineto
+ -254 -329 rmoveto
+ 74 vlineto
+ hintmask 1111010011010100
+ 132 -74 hlineto
+ -323 74 rmoveto
+ 126 -74 -126 hlineto
+ 126 195 rmoveto
+ -69 -126 69 vlineto
+ 323 hmoveto
+ -69 -132 69 vlineto
+ -387 -64 callsubr
+ -427 rmoveto
+ -58 249 -201 57 -65 -57 -194 -249 -67 -25 hlineto
+ -21 23 -25 25 -27 24 45 113 27 145 11 185 -43 6 rcurveline
+ -86 callsubr
+ -23 30 -26 30 -76 callsubr
+ 71 45 58 63 45 85 38 -37 32 -36 22 -30 45 60 rcurveline
+ -5 6 rlineto
+ 39 -155 68 155 323 -79 hlineto
+ -11 -4 -4 -11 vhcurveto
+ -13 -1 -40 0 -46 2 8 -18 11 -26 3 -18 rrcurveto
+ 62 41 1 10 26 hvcurveto
+ 25 11 7 19 35 vvcurveto
+ 79 58 vlineto
+ endchar
+ </CharString>
+ <CharString name="cid62172" fdSelectIndex="1">
+ 650 68 -70 callsubr
+ 223 69 6 54 77 66 cntrmask 01100100
+ hintmask 11110100
+ 81 367 rmoveto
+ 36 -97 callsubr
+ 639 186 rmoveto
+ 68 -427 vlineto
+ 4 37 3 39 3 39 -69 4 rcurveline
+ -3 -41 -3 -39 -4 -39 rrcurveto
+ -120 -68 -93 callgsubr
+ -26 -209 -49 -177 -98 -117 17 -8 35 -20 12 -10 99 131 50 184 29 226 rrcurveto
+ 340 -406 rmoveto
+ 27 56 33 88 25 73 -63 15 rcurveline
+ -16 -64 -33 -95 -27 -58 rrcurveto
+ hintmask 10001100
+ -277 -30 rmoveto
+ 43 53 16 90 10 83 -54 14 rcurveline
+ -9 -80 -19 -84 -40 -51 rrcurveto
+ 265 349 rmoveto
+ -66 -247 hlineto
+ -69 -35 -203 -212 -104 vhcurveto
+ 13 -15 19 -26 8 -16 167 86 61 154 12 68 12 -68 53 -158 155 -82 10 16 19 28 12 16 rrcurveto
+ -196 101 -32 206 66 vvcurveto
+ endchar
+ </CharString>
+ </CharStrings>
+ </CFFFont>
+
+ <GlobalSubrs>
+ <!-- The 'index' attribute is only for humans; it is ignored when parsed. -->
+ <CharString index="0">
+ 11 -104 callgsubr
+ return
+ </CharString>
+ <CharString index="1">
+ hintmask 000001000001000000000000
+ return
+ </CharString>
+ <CharString index="2">
+ hintmask 000000010000010000000000
+ return
+ </CharString>
+ <CharString index="3">
+ 8 18 37 vvcurveto
+ return
+ </CharString>
+ <CharString index="4">
+ 32.5 return
+ </CharString>
+ <CharString index="5">
+ -46.5 return
+ </CharString>
+ <CharString index="6">
+ 62 vlineto
+ endchar
+ </CharString>
+ <CharString index="7">
+ -11 -2 rlineto
+ return
+ </CharString>
+ <CharString index="8">
+ rcurveline
+ -98 callgsubr
+ return
+ </CharString>
+ <CharString index="9">
+ -12 -2 rlineto
+ return
+ </CharString>
+ <CharString index="10">
+ 162 hlineto
+ return
+ </CharString>
+ <CharString index="11">
+ 127 hlineto
+ return
+ </CharString>
+ <CharString index="12">
+ 110 vlineto
+ return
+ </CharString>
+ <CharString index="13">
+ 117 hlineto
+ return
+ </CharString>
+ <CharString index="14">
+ 112 hlineto
+ return
+ </CharString>
+ <CharString index="15">
+ 113 hlineto
+ return
+ </CharString>
+ <CharString index="16">
+ -136 hlineto
+ return
+ </CharString>
+ <CharString index="17">
+ 163 hlineto
+ return
+ </CharString>
+ <CharString index="18">
+ 155 rmoveto
+ return
+ </CharString>
+ <CharString index="19">
+ 195 hlineto
+ return
+ </CharString>
+ <CharString index="20">
+ hintmask 1111111111000000
+ return
+ </CharString>
+ <CharString index="21">
+ -132 hlineto
+ return
+ </CharString>
+ <CharString index="22">
+ -150 hlineto
+ return
+ </CharString>
+ <CharString index="23">
+ 122 hlineto
+ return
+ </CharString>
+ <CharString index="24">
+ 171 hlineto
+ return
+ </CharString>
+ </GlobalSubrs>
+ </CFF>
+
+ <GSUB>
+ <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="hang"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="1"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=2 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="aalt"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ <FeatureRecord index="1">
+ <FeatureTag value="jp90"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=2 -->
+ <Lookup index="0">
+ <LookupType value="3"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <AlternateSubst index="0">
+ <AlternateSet glyph="cid12223">
+ <Alternate glyph="cid62031"/>
+ <Alternate glyph="cid62033"/>
+ <Alternate glyph="cid61789"/>
+ </AlternateSet>
+ <AlternateSet glyph="cid12342">
+ <Alternate glyph="cid62039"/>
+ <Alternate glyph="cid59592"/>
+ </AlternateSet>
+ <AlternateSet glyph="cid15450">
+ <Alternate glyph="cid62104"/>
+ <Alternate glyph="cid62105"/>
+ </AlternateSet>
+ <AlternateSet glyph="cid18153">
+ <Alternate glyph="cid62172"/>
+ <Alternate glyph="cid61811"/>
+ </AlternateSet>
+ <AlternateSet glyph="cid61789">
+ <Alternate glyph="cid62031"/>
+ <Alternate glyph="cid62033"/>
+ <Alternate glyph="cid12223"/>
+ </AlternateSet>
+ <AlternateSet glyph="cid61811">
+ <Alternate glyph="cid62172"/>
+ <Alternate glyph="cid18153"/>
+ </AlternateSet>
+ </AlternateSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="cid12223" out="cid61789"/>
+ <Substitution in="cid18153" out="cid61811"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+ <VORG>
+ <majorVersion value="1"/>
+ <minorVersion value="0"/>
+ <defaultVertOriginY value="880"/>
+ <numVertOriginYMetrics value="0"/>
+ </VORG>
+
+ <hmtx>
+ <mtx name=".notdef" width="1000" lsb="100"/>
+ <mtx name="cid12223" width="1000" lsb="38"/>
+ <mtx name="cid12224" width="1000" lsb="40"/>
+ <mtx name="cid12342" width="1000" lsb="77"/>
+ <mtx name="cid15450" width="1000" lsb="44"/>
+ <mtx name="cid15451" width="1000" lsb="30"/>
+ <mtx name="cid15452" width="1000" lsb="39"/>
+ <mtx name="cid18153" width="1000" lsb="31"/>
+ <mtx name="cid18154" width="1000" lsb="43"/>
+ <mtx name="cid18155" width="1000" lsb="43"/>
+ <mtx name="cid59592" width="1000" lsb="79"/>
+ <mtx name="cid59593" width="1000" lsb="78"/>
+ <mtx name="cid61789" width="1000" lsb="31"/>
+ <mtx name="cid61811" width="1000" lsb="31"/>
+ <mtx name="cid62031" width="1000" lsb="38"/>
+ <mtx name="cid62032" width="1000" lsb="38"/>
+ <mtx name="cid62033" width="1000" lsb="36"/>
+ <mtx name="cid62034" width="1000" lsb="38"/>
+ <mtx name="cid62039" width="1000" lsb="79"/>
+ <mtx name="cid62104" width="1000" lsb="44"/>
+ <mtx name="cid62105" width="1000" lsb="44"/>
+ <mtx name="cid62172" width="1000" lsb="31"/>
+ </hmtx>
+
+ <vhea>
+ <tableVersion value="0x00011000"/>
+ <ascent value="500"/>
+ <descent value="-500"/>
+ <lineGap value="0"/>
+ <advanceHeightMax value="1000"/>
+ <minTopSideBearing value="42"/>
+ <minBottomSideBearing value="38"/>
+ <yMaxExtent value="962"/>
+ <caretSlopeRise value="0"/>
+ <caretSlopeRun value="1"/>
+ <caretOffset value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <reserved4 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfVMetrics value="1"/>
+ </vhea>
+
+ <vmtx>
+ <mtx name=".notdef" height="1000" tsb="0"/>
+ <mtx name="cid12223" height="1000" tsb="97"/>
+ <mtx name="cid12224" height="1000" tsb="91"/>
+ <mtx name="cid12342" height="1000" tsb="65"/>
+ <mtx name="cid15450" height="1000" tsb="44"/>
+ <mtx name="cid15451" height="1000" tsb="45"/>
+ <mtx name="cid15452" height="1000" tsb="45"/>
+ <mtx name="cid18153" height="1000" tsb="43"/>
+ <mtx name="cid18154" height="1000" tsb="42"/>
+ <mtx name="cid18155" height="1000" tsb="42"/>
+ <mtx name="cid59592" height="1000" tsb="60"/>
+ <mtx name="cid59593" height="1000" tsb="60"/>
+ <mtx name="cid61789" height="1000" tsb="91"/>
+ <mtx name="cid61811" height="1000" tsb="43"/>
+ <mtx name="cid62031" height="1000" tsb="101"/>
+ <mtx name="cid62032" height="1000" tsb="97"/>
+ <mtx name="cid62033" height="1000" tsb="91"/>
+ <mtx name="cid62034" height="1000" tsb="101"/>
+ <mtx name="cid62039" height="1000" tsb="45"/>
+ <mtx name="cid62104" height="1000" tsb="44"/>
+ <mtx name="cid62105" height="1000" tsb="44"/>
+ <mtx name="cid62172" height="1000" tsb="43"/>
+ </vmtx>
+
+</ttFont>
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index d195af0f..7efcb698 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -1,5 +1,6 @@
import io
-from fontTools.misc.testTools import getXML
+import fontTools.ttLib.tables.otBase
+from fontTools.misc.testTools import getXML, stripVariableItemsFromTTX
from fontTools.misc.textTools import tobytes, tostr
from fontTools import subset
from fontTools.fontBuilder import FontBuilder
@@ -19,44 +20,36 @@ import pathlib
import pytest
-class SubsetTest(unittest.TestCase):
- def __init__(self, methodName):
- unittest.TestCase.__init__(self, methodName)
- # Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
- # and fires deprecation warnings if a program uses the old name.
- if not hasattr(self, "assertRaisesRegex"):
- self.assertRaisesRegex = self.assertRaisesRegexp
+class SubsetTest:
+ @classmethod
+ def setup_class(cls):
+ cls.tempdir = None
+ cls.num_tempfiles = 0
- def setUp(self):
- self.tempdir = None
- self.num_tempfiles = 0
-
- def tearDown(self):
- if self.tempdir:
- shutil.rmtree(self.tempdir)
+ @classmethod
+ def teardown_class(cls):
+ if cls.tempdir:
+ shutil.rmtree(cls.tempdir, ignore_errors=True)
@staticmethod
def getpath(testfile):
path, _ = os.path.split(__file__)
return os.path.join(path, "data", testfile)
- def temp_path(self, suffix):
- if not self.tempdir:
- self.tempdir = tempfile.mkdtemp()
- self.num_tempfiles += 1
- return os.path.join(self.tempdir,
- "tmp%d%s" % (self.num_tempfiles, suffix))
-
- 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
+ @classmethod
+ def temp_path(cls, suffix):
+ if not cls.tempdir:
+ cls.tempdir = tempfile.mkdtemp()
+ cls.num_tempfiles += 1
+ return os.path.join(cls.tempdir,
+ "tmp%d%s" % (cls.num_tempfiles, suffix))
+
+ @staticmethod
+ def read_ttx(path):
+ with open(path, "r", encoding="utf-8") as f:
+ ttx = f.read()
+ # don't care whether TTF or OTF, thus strip sfntVersion as well
+ return stripVariableItemsFromTTX(ttx, sfntVersion=True).splitlines(True)
def expect_ttx(self, font, expected_ttx, tables=None):
path = self.temp_path(suffix=".ttx")
@@ -67,21 +60,21 @@ class SubsetTest(unittest.TestCase):
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")
+ pytest.fail("TTX output is different from expected")
def compile_font(self, path, suffix):
savepath = self.temp_path(suffix=suffix)
font = TTFont(recalcBBoxes=False, recalcTimestamp=False)
font.importXML(path)
font.save(savepath, reorderTables=None)
- return font, savepath
+ return savepath
# -----
# Tests
# -----
def test_layout_scripts(self):
- _, fontpath = self.compile_font(self.getpath("layout_scripts.ttx"), ".otf")
+ fontpath = self.compile_font(self.getpath("layout_scripts.ttx"), ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--glyphs=*", "--layout-features=*",
"--layout-scripts=latn,arab.URD,arab.dflt",
@@ -91,41 +84,41 @@ class SubsetTest(unittest.TestCase):
["GPOS", "GSUB"])
def test_no_notdef_outline_otf(self):
- _, fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
+ fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--no-notdef-outline", "--gids=0", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_no_notdef_outline_otf.ttx"), ["CFF "])
def test_no_notdef_outline_cid(self):
- _, fontpath = self.compile_font(self.getpath("TestCID-Regular.ttx"), ".otf")
+ fontpath = self.compile_font(self.getpath("TestCID-Regular.ttx"), ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--no-notdef-outline", "--gids=0", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_no_notdef_outline_cid.ttx"), ["CFF "])
def test_no_notdef_outline_ttf(self):
- _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--no-notdef-outline", "--gids=0", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_no_notdef_outline_ttf.ttx"), ["glyf", "hmtx"])
def test_subset_ankr(self):
- _, fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_ankr.ttx"), ["ankr"])
def test_subset_ankr_remove(self):
- _, fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=two", "--output-file=%s" % subsetpath])
- self.assertNotIn("ankr", TTFont(subsetpath))
+ assert "ankr" not in TTFont(subsetpath)
def test_subset_bsln_format_0(self):
- _, fontpath = self.compile_font(self.getpath("TestBSLN-0.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestBSLN-0.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -138,7 +131,7 @@ class SubsetTest(unittest.TestCase):
# a subsetted font with {zero, one} and the implicit .notdef, all
# glyphs in the resulting font use the Roman baseline. In this case,
# we expect a format 0 'bsln' table because it is the most compact.
- _, fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030-0031",
"--output-file=%s" % subsetpath])
@@ -154,7 +147,7 @@ class SubsetTest(unittest.TestCase):
# subsetted font, we expect a format 1 'bsln' table whose default
# is Roman, but with an override that uses the ideographic baseline
# for uni2EA2.
- _, fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030-0031,U+2EA2",
"--output-file=%s" % subsetpath])
@@ -165,7 +158,7 @@ class SubsetTest(unittest.TestCase):
# The 'bsln' table in TestBSLN-2 refers to control points in glyph 'P'
# for defining its baselines. Therefore, the subsetted font should
# include this glyph even though it is not requested explicitly.
- _, fontpath = self.compile_font(self.getpath("TestBSLN-2.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestBSLN-2.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -179,7 +172,7 @@ class SubsetTest(unittest.TestCase):
# baseline measurement, all glyphs in the resulting font use the Roman
# baseline. In this case, we expect a format 2 'bsln' table because it
# is the most compact encoding.
- _, fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030",
"--output-file=%s" % subsetpath])
@@ -195,7 +188,7 @@ class SubsetTest(unittest.TestCase):
# subsetted font, we expect a format 1 'bsln' table whose default
# is Roman, but with an override that uses the ideographic baseline
# for uni2EA2.
- _, fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030-0031,U+2EA2",
"--output-file=%s" % subsetpath])
@@ -203,35 +196,35 @@ class SubsetTest(unittest.TestCase):
self.expect_ttx(subsetfont, self.getpath("expect_bsln_3.ttx"), ["bsln"])
def test_subset_clr(self):
- _, fontpath = self.compile_font(self.getpath("TestCLR-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestCLR-Regular.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=smileface", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_keep_colr.ttx"), ["GlyphOrder", "hmtx", "glyf", "COLR", "CPAL"])
def test_subset_gvar(self):
- _, fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+002B,U+2212", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_keep_gvar.ttx"), ["GlyphOrder", "avar", "fvar", "gvar", "name"])
def test_subset_gvar_notdef_outline(self):
- _, fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030", "--notdef_outline", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_keep_gvar_notdef_outline.ttx"), ["GlyphOrder", "avar", "fvar", "gvar", "name"])
def test_subset_lcar_remove(self):
- _, fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
- self.assertNotIn("lcar", subsetfont)
+ assert "lcar" not in subsetfont
def test_subset_lcar_format_0(self):
- _, fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+FB01",
"--output-file=%s" % subsetpath])
@@ -239,7 +232,7 @@ class SubsetTest(unittest.TestCase):
self.expect_ttx(subsetfont, self.getpath("expect_lcar_0.ttx"), ["lcar"])
def test_subset_lcar_format_1(self):
- _, fontpath = self.compile_font(self.getpath("TestLCAR-1.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestLCAR-1.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+FB01",
"--output-file=%s" % subsetpath])
@@ -247,14 +240,14 @@ class SubsetTest(unittest.TestCase):
self.expect_ttx(subsetfont, self.getpath("expect_lcar_1.ttx"), ["lcar"])
def test_subset_math(self):
- _, fontpath = self.compile_font(self.getpath("TestMATH-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestMATH-Regular.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0041,U+0028,U+0302,U+1D400,U+1D435", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_keep_math.ttx"), ["GlyphOrder", "CFF ", "MATH", "hmtx"])
def test_subset_math_partial(self):
- _, fontpath = self.compile_font(self.getpath("test_math_partial.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("test_math_partial.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--text=A", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -265,21 +258,21 @@ class SubsetTest(unittest.TestCase):
# the Optical Bounds table. When subsetting, we do not request any
# of those glyphs. Therefore, the produced subsetted font should
# not contain an 'opbd' table.
- _, fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
- self.assertNotIn("opbd", subsetfont)
+ assert "opbd" not in subsetfont
def test_subset_opbd_format_0(self):
- _, fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_opbd_0.ttx"), ["opbd"])
def test_subset_opbd_format_1(self):
- _, fontpath = self.compile_font(self.getpath("TestOPBD-1.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestOPBD-1.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -288,12 +281,12 @@ class SubsetTest(unittest.TestCase):
def test_subset_prop_remove_default_zero(self):
# If all glyphs have an AAT glyph property with value 0,
# the "prop" table should be removed from the subsetted font.
- _, fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0041",
"--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
- self.assertNotIn("prop", subsetfont)
+ assert "prop" not in subsetfont
def test_subset_prop_0(self):
# If all glyphs share the same AAT glyph properties, the "prop" table
@@ -302,7 +295,7 @@ class SubsetTest(unittest.TestCase):
# Unless the shared value is zero, in which case the subsetted font
# should have no "prop" table at all. But that case has already been
# tested above in test_subset_prop_remove_default_zero().
- _, fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030-0032", "--no-notdef-glyph",
"--output-file=%s" % subsetpath])
@@ -313,7 +306,7 @@ class SubsetTest(unittest.TestCase):
# If not all glyphs share the same AAT glyph properties, the subsetted
# font should contain a "prop" table in format 1. To save space, the
# DefaultProperties should be set to the most frequent value.
- _, fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=U+0030-0032", "--notdef-outline",
"--output-file=%s" % subsetpath])
@@ -323,32 +316,32 @@ class SubsetTest(unittest.TestCase):
def test_options(self):
# https://github.com/fonttools/fonttools/issues/413
opt1 = subset.Options()
- self.assertTrue('Xyz-' not in opt1.layout_features)
+ assert 'Xyz-' not in opt1.layout_features
opt2 = subset.Options()
opt2.layout_features.append('Xyz-')
- self.assertTrue('Xyz-' in opt2.layout_features)
- self.assertTrue('Xyz-' not in opt1.layout_features)
+ assert 'Xyz-' in opt2.layout_features
+ assert 'Xyz-' not in opt1.layout_features
def test_google_color(self):
- _, fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
- self.assertTrue("CBDT" in subsetfont)
- self.assertTrue("CBLC" in subsetfont)
- self.assertTrue("x" in subsetfont['CBDT'].strikeData[0])
- self.assertFalse("y" in subsetfont['CBDT'].strikeData[0])
+ assert "CBDT" in subsetfont
+ assert "CBLC" in subsetfont
+ assert "x" in subsetfont['CBDT'].strikeData[0]
+ assert "y" not in subsetfont['CBDT'].strikeData[0]
def test_google_color_all(self):
- _, fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
- self.assertTrue("x" in subsetfont['CBDT'].strikeData[0])
- self.assertTrue("y" in subsetfont['CBDT'].strikeData[0])
+ assert "x" in subsetfont['CBDT'].strikeData[0]
+ assert "y" in subsetfont['CBDT'].strikeData[0]
def test_sbix(self):
- _, fontpath = self.compile_font(self.getpath("sbix.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("sbix.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -356,7 +349,7 @@ class SubsetTest(unittest.TestCase):
"expect_sbix.ttx"), ["sbix"])
def test_timing_publishes_parts(self):
- _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
options = subset.Options()
options.timing = True
@@ -367,15 +360,15 @@ class SubsetTest(unittest.TestCase):
subsetter.subset(font)
logs = captor.records
- self.assertTrue(len(logs) > 5)
- self.assertEqual(len(logs), len([l for l in logs if 'msg' in l.args and 'time' in l.args]))
+ assert len(logs) > 5
+ assert len(logs) == len([l for l in logs if 'msg' in l.args and 'time' in l.args])
# Look for a few things we know should happen
- self.assertTrue(filter(lambda l: l.args['msg'] == "load 'cmap'", logs))
- self.assertTrue(filter(lambda l: l.args['msg'] == "subset 'cmap'", logs))
- self.assertTrue(filter(lambda l: l.args['msg'] == "subset 'glyf'", logs))
+ assert filter(lambda l: l.args['msg'] == "load 'cmap'", logs)
+ assert filter(lambda l: l.args['msg'] == "subset 'cmap'", logs)
+ assert filter(lambda l: l.args['msg'] == "subset 'glyf'", logs)
def test_passthrough_tables(self):
- _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
font = TTFont(fontpath)
unknown_tag = 'ZZZZ'
unknown_table = newTable(unknown_tag)
@@ -388,17 +381,17 @@ class SubsetTest(unittest.TestCase):
subsetfont = TTFont(subsetpath)
# tables we can't subset are dropped by default
- self.assertFalse(unknown_tag in subsetfont)
+ assert unknown_tag not in subsetfont
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--passthrough-tables", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
# unknown tables are kept if --passthrough-tables option is passed
- self.assertTrue(unknown_tag in subsetfont)
+ assert unknown_tag in subsetfont
def test_non_BMP_text_arg_input(self):
- _, fontpath = self.compile_font(
+ fontpath = self.compile_font(
self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
text = tostr(u"A\U0001F6D2", encoding='utf-8')
@@ -406,11 +399,11 @@ class SubsetTest(unittest.TestCase):
subset.main([fontpath, "--text=%s" % text, "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
- self.assertEqual(subsetfont['maxp'].numGlyphs, 3)
- self.assertEqual(subsetfont.getGlyphOrder(), ['.notdef', 'A', 'u1F6D2'])
+ assert subsetfont['maxp'].numGlyphs == 3
+ assert subsetfont.getGlyphOrder() == ['.notdef', 'A', 'u1F6D2']
def test_non_BMP_text_file_input(self):
- _, fontpath = self.compile_font(
+ fontpath = self.compile_font(
self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
text = tobytes(u"A\U0001F6D2", encoding='utf-8')
@@ -424,12 +417,12 @@ class SubsetTest(unittest.TestCase):
finally:
os.remove(tmp.name)
- self.assertEqual(subsetfont['maxp'].numGlyphs, 3)
- self.assertEqual(subsetfont.getGlyphOrder(), ['.notdef', 'A', 'u1F6D2'])
+ assert subsetfont['maxp'].numGlyphs == 3
+ assert subsetfont.getGlyphOrder() == ['.notdef', 'A', 'u1F6D2']
def test_no_hinting_CFF(self):
ttxpath = self.getpath("Lobster.subset.ttx")
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--no-hinting", "--notdef-outline",
"--output-file=%s" % subsetpath, "*"])
@@ -439,7 +432,7 @@ class SubsetTest(unittest.TestCase):
def test_desubroutinize_CFF(self):
ttxpath = self.getpath("Lobster.subset.ttx")
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--desubroutinize", "--notdef-outline",
"--output-file=%s" % subsetpath, "*"])
@@ -449,7 +442,7 @@ class SubsetTest(unittest.TestCase):
def test_desubroutinize_hinted_subrs_CFF(self):
ttxpath = self.getpath("test_hinted_subrs_CFF.ttx")
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--desubroutinize", "--notdef-outline",
"--output-file=%s" % subsetpath, "*"])
@@ -459,7 +452,7 @@ class SubsetTest(unittest.TestCase):
def test_desubroutinize_cntrmask_CFF(self):
ttxpath = self.getpath("test_cntrmask_CFF.ttx")
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--desubroutinize", "--notdef-outline",
"--output-file=%s" % subsetpath, "*"])
@@ -469,7 +462,7 @@ class SubsetTest(unittest.TestCase):
def test_no_hinting_desubroutinize_CFF(self):
ttxpath = self.getpath("test_hinted_subrs_CFF.ttx")
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--no-hinting", "--desubroutinize", "--notdef-outline",
"--output-file=%s" % subsetpath, "*"])
@@ -478,7 +471,7 @@ class SubsetTest(unittest.TestCase):
"expect_no_hinting_desubroutinize_CFF.ttx"), ["CFF "])
def test_no_hinting_TTF(self):
- _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--no-hinting", "--notdef-outline",
"--output-file=%s" % subsetpath, "*"])
@@ -486,11 +479,11 @@ class SubsetTest(unittest.TestCase):
self.expect_ttx(subsetfont, self.getpath(
"expect_no_hinting_TTF.ttx"), ["glyf", "maxp"])
for tag in subset.Options().hinting_tables:
- self.assertTrue(tag not in subsetfont)
+ assert tag not in subsetfont
def test_notdef_width_cid(self):
# https://github.com/fonttools/fonttools/pull/845
- _, fontpath = self.compile_font(self.getpath("NotdefWidthCID-Regular.ttx"), ".otf")
+ fontpath = self.compile_font(self.getpath("NotdefWidthCID-Regular.ttx"), ".otf")
subsetpath = self.temp_path(".otf")
subset.main([fontpath, "--no-notdef-outline", "--gids=0,1", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -503,18 +496,18 @@ class SubsetTest(unittest.TestCase):
head = font['head']
bounds = [head.xMin, head.yMin, head.xMax, head.yMax]
- _, fontpath = self.compile_font(ttxpath, ".ttf")
+ fontpath = self.compile_font(ttxpath, ".ttf")
subsetpath = self.temp_path(".ttf")
# by default, the subsetter does not recalculate the bounding box
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
head = TTFont(subsetpath)['head']
- self.assertEqual(bounds, [head.xMin, head.yMin, head.xMax, head.yMax])
+ assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
subset.main([fontpath, "--recalc-bounds", "--output-file=%s" % subsetpath, "*"])
head = TTFont(subsetpath)['head']
bounds = [132, 304, 365, 567]
- self.assertEqual(bounds, [head.xMin, head.yMin, head.xMax, head.yMax])
+ assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
def test_recalc_bounds_otf(self):
ttxpath = self.getpath("TestOTF-Regular.ttx")
@@ -523,76 +516,76 @@ class SubsetTest(unittest.TestCase):
head = font['head']
bounds = [head.xMin, head.yMin, head.xMax, head.yMax]
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
# by default, the subsetter does not recalculate the bounding box
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
head = TTFont(subsetpath)['head']
- self.assertEqual(bounds, [head.xMin, head.yMin, head.xMax, head.yMax])
+ assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
subset.main([fontpath, "--recalc-bounds", "--output-file=%s" % subsetpath, "*"])
head = TTFont(subsetpath)['head']
bounds = [132, 304, 365, 567]
- self.assertEqual(bounds, [head.xMin, head.yMin, head.xMax, head.yMax])
+ assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
def test_recalc_timestamp_ttf(self):
ttxpath = self.getpath("TestTTF-Regular.ttx")
font = TTFont()
font.importXML(ttxpath)
modified = font['head'].modified
- _, fontpath = self.compile_font(ttxpath, ".ttf")
+ fontpath = self.compile_font(ttxpath, ".ttf")
subsetpath = self.temp_path(".ttf")
# by default, the subsetter does not recalculate the modified timestamp
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
- self.assertEqual(modified, TTFont(subsetpath)['head'].modified)
+ assert modified == TTFont(subsetpath)['head'].modified
subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"])
- self.assertLess(modified, TTFont(subsetpath)['head'].modified)
+ assert modified < TTFont(subsetpath)['head'].modified
def test_recalc_timestamp_otf(self):
ttxpath = self.getpath("TestOTF-Regular.ttx")
font = TTFont()
font.importXML(ttxpath)
modified = font['head'].modified
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
# by default, the subsetter does not recalculate the modified timestamp
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
- self.assertEqual(modified, TTFont(subsetpath)['head'].modified)
+ assert modified == TTFont(subsetpath)['head'].modified
subset.main([fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"])
- self.assertLess(modified, TTFont(subsetpath)['head'].modified)
+ assert modified < TTFont(subsetpath)['head'].modified
def test_recalc_max_context(self):
ttxpath = self.getpath("Lobster.subset.ttx")
font = TTFont()
font.importXML(ttxpath)
max_context = font['OS/2'].usMaxContext
- _, fontpath = self.compile_font(ttxpath, ".otf")
+ fontpath = self.compile_font(ttxpath, ".otf")
subsetpath = self.temp_path(".otf")
# by default, the subsetter does not recalculate the usMaxContext
subset.main([fontpath, "--drop-tables+=GSUB,GPOS",
"--output-file=%s" % subsetpath])
- self.assertEqual(max_context, TTFont(subsetpath)['OS/2'].usMaxContext)
+ assert max_context == TTFont(subsetpath)['OS/2'].usMaxContext
subset.main([fontpath, "--recalc-max-context",
"--drop-tables+=GSUB,GPOS",
"--output-file=%s" % subsetpath])
- self.assertEqual(0, TTFont(subsetpath)['OS/2'].usMaxContext)
+ assert 0 == TTFont(subsetpath)['OS/2'].usMaxContext
def test_retain_gids_ttf(self):
- _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
font = TTFont(fontpath)
- self.assertEqual(font["hmtx"]["A"], (500, 132))
- self.assertEqual(font["hmtx"]["B"], (400, 132))
+ assert font["hmtx"]["A"] == (500, 132)
+ assert font["hmtx"]["B"] == (400, 132)
- self.assertGreater(font["glyf"]["A"].numberOfContours, 0)
- self.assertGreater(font["glyf"]["B"].numberOfContours, 0)
+ assert font["glyf"]["A"].numberOfContours > 0
+ assert font["glyf"]["B"].numberOfContours > 0
subsetpath = self.temp_path(".ttf")
subset.main(
@@ -606,29 +599,29 @@ class SubsetTest(unittest.TestCase):
)
subsetfont = TTFont(subsetpath)
- self.assertEqual(subsetfont.getGlyphOrder(), font.getGlyphOrder()[0:3])
+ assert subsetfont.getGlyphOrder() == font.getGlyphOrder()[0:3]
hmtx = subsetfont["hmtx"]
- self.assertEqual(hmtx["A"], ( 0, 0))
- self.assertEqual(hmtx["B"], (400, 132))
+ assert hmtx["A"] == (0, 0)
+ assert hmtx["B"] == (400, 132)
glyf = subsetfont["glyf"]
- self.assertEqual(glyf["A"].numberOfContours, 0)
- self.assertGreater(glyf["B"].numberOfContours, 0)
+ assert glyf["A"].numberOfContours == 0
+ assert glyf["B"].numberOfContours > 0
def test_retain_gids_cff(self):
- _, fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
+ fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
font = TTFont(fontpath)
- self.assertEqual(font["hmtx"]["A"], (500, 132))
- self.assertEqual(font["hmtx"]["B"], (400, 132))
- self.assertEqual(font["hmtx"]["C"], (500, 0))
+ assert font["hmtx"]["A"] == (500, 132)
+ assert font["hmtx"]["B"] == (400, 132)
+ assert font["hmtx"]["C"] == (500, 0)
font["CFF "].cff[0].decompileAllCharStrings()
cs = font["CFF "].cff[0].CharStrings
- self.assertGreater(len(cs["A"].program), 0)
- self.assertGreater(len(cs["B"].program), 0)
- self.assertGreater(len(cs["C"].program), 0)
+ assert len(cs["A"].program) > 0
+ assert len(cs["B"].program) > 0
+ assert len(cs["C"].program) > 0
subsetpath = self.temp_path(".otf")
subset.main(
@@ -642,29 +635,30 @@ class SubsetTest(unittest.TestCase):
)
subsetfont = TTFont(subsetpath)
- self.assertEqual(subsetfont.getGlyphOrder(), font.getGlyphOrder()[0:3])
+ assert subsetfont.getGlyphOrder() == font.getGlyphOrder()[0:3]
hmtx = subsetfont["hmtx"]
- self.assertEqual(hmtx["A"], (0, 0))
- self.assertEqual(hmtx["B"], (400, 132))
+ assert hmtx["A"] == (0, 0)
+ assert hmtx["B"] == (400, 132)
subsetfont["CFF "].cff[0].decompileAllCharStrings()
cs = subsetfont["CFF "].cff[0].CharStrings
- self.assertEqual(cs["A"].program, ["endchar"])
- self.assertGreater(len(cs["B"].program), 0)
+ assert cs["A"].program == ["endchar"]
+ assert len(cs["B"].program) > 0
def test_retain_gids_cff2(self):
ttx_path = self.getpath("../../varLib/data/master_ttx_varfont_otf/TestCFF2VF.ttx")
- font, fontpath = self.compile_font(ttx_path, ".otf")
+ fontpath = self.compile_font(ttx_path, ".otf")
+ font = TTFont(fontpath)
- self.assertEqual(font["hmtx"]["A"], (600, 31))
- self.assertEqual(font["hmtx"]["T"], (600, 41))
+ assert font["hmtx"]["A"] == (600, 31)
+ assert font["hmtx"]["T"] == (600, 41)
font["CFF2"].cff[0].decompileAllCharStrings()
cs = font["CFF2"].cff[0].CharStrings
- self.assertGreater(len(cs["A"].program), 0)
- self.assertGreater(len(cs["T"].program), 0)
+ assert len(cs["A"].program) > 0
+ assert len(cs["T"].program) > 0
subsetpath = self.temp_path(".otf")
subset.main(
@@ -677,33 +671,33 @@ class SubsetTest(unittest.TestCase):
)
subsetfont = TTFont(subsetpath)
- self.assertEqual(len(subsetfont.getGlyphOrder()), len(font.getGlyphOrder()[0:3]))
+ assert len(subsetfont.getGlyphOrder()) == len(font.getGlyphOrder()[0:3])
hmtx = subsetfont["hmtx"]
- self.assertEqual(hmtx["glyph00001"], ( 0, 0))
- self.assertEqual(hmtx["T"], (600, 41))
+ assert hmtx["glyph00001"] == (0, 0)
+ assert hmtx["T"] == (600, 41)
subsetfont["CFF2"].cff[0].decompileAllCharStrings()
cs = subsetfont["CFF2"].cff[0].CharStrings
- self.assertEqual(cs["glyph00001"].program, [])
- self.assertGreater(len(cs["T"].program), 0)
+ assert cs["glyph00001"].program == []
+ assert len(cs["T"].program) > 0
def test_HVAR_VVAR(self):
- _, fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--text=BD", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_HVVAR.ttx"), ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"])
def test_HVAR_VVAR_retain_gids(self):
- _, fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--text=BD", "--retain-gids", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("expect_HVVAR_retain_gids.ttx"), ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"])
def test_subset_flavor(self):
- _, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
font = TTFont(fontpath)
woff_path = self.temp_path(".woff")
@@ -717,7 +711,7 @@ class SubsetTest(unittest.TestCase):
)
woff = TTFont(woff_path)
- self.assertEqual(woff.flavor, "woff")
+ assert woff.flavor == "woff"
woff2_path = self.temp_path(".woff2")
subset.main(
@@ -730,7 +724,7 @@ class SubsetTest(unittest.TestCase):
)
woff2 = TTFont(woff2_path)
- self.assertEqual(woff2.flavor, "woff2")
+ assert woff2.flavor == "woff2"
ttf_path = self.temp_path(".ttf")
subset.main(
@@ -742,13 +736,13 @@ class SubsetTest(unittest.TestCase):
)
ttf = TTFont(ttf_path)
- self.assertEqual(ttf.flavor, None)
+ assert ttf.flavor is None
def test_subset_context_subst_format_3(self):
# https://github.com/fonttools/fonttools/issues/1879
# Test font contains 'calt' feature with Format 3 ContextSubst lookup subtables
ttx = self.getpath("TestContextSubstFormat3.ttx")
- font, fontpath = self.compile_font(ttx, ".ttf")
+ fontpath = self.compile_font(ttx, ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
@@ -756,13 +750,14 @@ class SubsetTest(unittest.TestCase):
self.expect_ttx(subsetfont, ttx)
def test_cmap_prune_format12(self):
- _, fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf")
+ fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf")
subsetpath = self.temp_path(".ttf")
subset.main([fontpath, "--glyphs=a", "--output-file=%s" % subsetpath])
subsetfont = TTFont(subsetpath)
self.expect_ttx(subsetfont, self.getpath("CmapSubsetTest.subset.ttx"), ["cmap"])
- def test_GPOS_PairPos_Format2_useClass0(self):
+ @pytest.mark.parametrize("text, n", [("!", 1), ("#", 2)])
+ def test_GPOS_PairPos_Format2_useClass0(self, text, n):
# Check two things related to class 0 ('every other glyph'):
# 1) that it's reused for ClassDef1 when it becomes empty as the subset glyphset
# is intersected with the table's Coverage
@@ -772,29 +767,27 @@ class SubsetTest(unittest.TestCase):
# The test font (from Harfbuzz test suite) is constructed to trigger these two
# situations depending on the input subset --text.
# https://github.com/fonttools/fonttools/pull/2221
- _, fontpath = self.compile_font(
+ fontpath = self.compile_font(
self.getpath("GPOS_PairPos_Format2_PR_2221.ttx"), ".ttf"
)
subsetpath = self.temp_path(".ttf")
- for n, text in enumerate("!#", start=1):
- expected_ttx = self.getpath(
- f"GPOS_PairPos_Format2_ClassDef{n}_useClass0.subset.ttx"
- )
- with self.subTest(text=text, expected_ttx=expected_ttx):
- subset.main(
- [
- fontpath,
- f"--text='{text}'",
- "--layout-features+=test",
- "--output-file=%s" % subsetpath,
- ]
- )
- subsetfont = TTFont(subsetpath)
- self.expect_ttx(subsetfont, expected_ttx, ["GPOS"])
+ expected_ttx = self.getpath(
+ f"GPOS_PairPos_Format2_ClassDef{n}_useClass0.subset.ttx"
+ )
+ subset.main(
+ [
+ fontpath,
+ f"--text='{text}'",
+ "--layout-features+=test",
+ "--output-file=%s" % subsetpath,
+ ]
+ )
+ subsetfont = TTFont(subsetpath)
+ self.expect_ttx(subsetfont, expected_ttx, ["GPOS"])
def test_GPOS_SinglePos_prune_post_subset_no_value(self):
- _, fontpath = self.compile_font(
+ fontpath = self.compile_font(
self.getpath("GPOS_SinglePos_no_value_issue_2312.ttx"), ".ttf"
)
subsetpath = self.temp_path(".ttf")
@@ -806,6 +799,98 @@ class SubsetTest(unittest.TestCase):
["GlyphOrder", "GPOS"],
)
+ @pytest.mark.parametrize(
+ "installed, enabled, ok",
+ [
+ pytest.param(True, None, True, id="installed-auto-ok"),
+ pytest.param(True, None, False, id="installed-auto-fail"),
+ pytest.param(True, True, True, id="installed-enabled-ok"),
+ pytest.param(True, True, False, id="installed-enabled-fail"),
+ pytest.param(True, False, True, id="installed-disabled"),
+ pytest.param(False, True, True, id="not_installed-enabled"),
+ pytest.param(False, False, True, id="not_installed-disabled"),
+ ],
+ )
+ def test_harfbuzz_repacker(self, caplog, monkeypatch, installed, enabled, ok):
+ # Use a mock to test the pure-python serializer is used when uharfbuzz
+ # returns an error or is not installed
+ have_uharfbuzz = fontTools.ttLib.tables.otBase.have_uharfbuzz
+ if installed:
+ if not have_uharfbuzz:
+ pytest.skip("uharfbuzz is not installed")
+ if not ok:
+ # 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
+ monkeypatch.setattr(
+ fontTools.ttLib.tables.otBase, "have_uharfbuzz", False
+ )
+
+ fontpath = self.compile_font(self.getpath("harfbuzz_repacker.ttx"), ".otf")
+ subsetpath = self.temp_path(".otf")
+ args = [
+ fontpath,
+ "--unicodes=0x53a9",
+ "--layout-features=*",
+ f"--output-file={subsetpath}",
+ ]
+ if enabled is True:
+ args.append("--harfbuzz-repacker")
+ elif enabled is False:
+ args.append("--no-harfbuzz-repacker")
+ # elif enabled is None: ... is the default
+
+ if enabled is True and not installed:
+ # raise if enabled but not installed
+ with pytest.raises(ImportError, match="uharfbuzz"):
+ subset.main(args)
+ return
+
+ with caplog.at_level(logging.DEBUG, "fontTools.ttLib.tables.otBase"):
+ subset.main(args)
+
+ subsetfont = TTFont(subsetpath)
+ # both hb.repack and pure-python serializer compile to the same ttx
+ self.expect_ttx(
+ subsetfont, self.getpath("expect_harfbuzz_repacker.ttx"), ["GSUB"]
+ )
+
+ if enabled or enabled is None:
+ if installed:
+ assert "serializing 'GSUB' with hb.repack" in caplog.text
+
+ if enabled is None and not installed:
+ assert (
+ "uharfbuzz not found, compiling 'GSUB' with pure-python serializer"
+ ) in caplog.text
+
+ if enabled is False:
+ assert (
+ "hb.repack disabled, compiling 'GSUB' with pure-python serializer"
+ ) in caplog.text
+
+ # test we emit a log.error if hb.repack fails (and we don't if successful)
+ assert (
+ (
+ "hb.repack failed to serialize 'GSUB', attempting fonttools resolutions "
+ "; the error message was: RepackerError: mocking"
+ ) in caplog.text
+ ) ^ ok
+
@pytest.fixture
def featureVarsTestFont():
@@ -875,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"})
@@ -893,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) == [
@@ -940,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):
@@ -1348,7 +1477,7 @@ def test_subset_svg_missing_lxml(ttf_path):
font["SVG "].docList = [('<svg><g id="glyph1"/></svg>', 1, 1)]
font.save(ttf_path)
- with pytest.raises(ModuleNotFoundError):
+ with pytest.raises(ImportError):
subset.main([str(ttf_path), "--gids=0,1"])
diff --git a/Tests/svgLib/path/parser_test.py b/Tests/svgLib/path/parser_test.py
index b533dd8e..d33043fc 100644
--- a/Tests/svgLib/path/parser_test.py
+++ b/Tests/svgLib/path/parser_test.py
@@ -280,6 +280,15 @@ def test_exponents():
assert pen.value == expected
+ pen = RecordingPen()
+ parse_path("M-3e38 3E+38L-3E-38,3e-38", pen)
+ expected = [
+ ("moveTo", ((-3e+38, 3e+38),)),
+ ("lineTo", ((-3e-38, 3e-38),)),
+ ("endPath", ()),
+ ]
+
+ assert pen.value == expected
def test_invalid_implicit_command():
with pytest.raises(ValueError) as exc_info:
@@ -360,7 +369,7 @@ def test_arc_pen_with_arcTo():
"M1-2A3-4-1.0 01.5.7",
[
("moveTo", ((1.0, -2.0),)),
- ("arcTo", (3.0, -4.0, -1.0, False, True, (0.5, 0.7))),
+ ("arcTo", (3.0, 4.0, -1.0, False, True, (0.5, 0.7))),
("endPath", ()),
],
),
diff --git a/Tests/ttLib/data/I-512upem.ttx b/Tests/ttLib/data/I-512upem.ttx
new file mode 100644
index 00000000..34795b1f
--- /dev/null
+++ b/Tests/ttLib/data/I-512upem.ttx
@@ -0,0 +1,3777 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.36">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="I"/>
+ </GlyphOrder>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="475"/>
+ <descent value="-125"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="227"/>
+ <minLeftSideBearing value="44"/>
+ <minRightSideBearing value="44"/>
+ <xMaxExtent value="92"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="2"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="2"/>
+ <maxPoints value="4"/>
+ <maxContours value="1"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="286"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000000"/>
+ <ySubscriptXSize value="359"/>
+ <ySubscriptYSize value="333"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="72"/>
+ <ySuperscriptXSize value="359"/>
+ <ySuperscriptYSize value="333"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="244"/>
+ <yStrikeoutSize value="26"/>
+ <yStrikeoutPosition value="128"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="2"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="GOOG"/>
+ <fsSelection value="00000000 11000000"/>
+ <usFirstCharIndex value="73"/>
+ <usLastCharIndex value="73"/>
+ <sTypoAscender value="475"/>
+ <sTypoDescender value="-125"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="615"/>
+ <usWinDescent value="150"/>
+ <ulCodePageRange1 value="00100000 00000000 00000001 10011111"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="263"/>
+ <sCapHeight value="364"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="4"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="227" lsb="25"/>
+ <mtx name="I" width="136" lsb="44"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x49" name="I"/><!-- LATIN CAPITAL LETTER I -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x49" name="I"/><!-- LATIN CAPITAL LETTER I -->
+ </cmap_format_4>
+ </cmap>
+
+ <prep>
+ <assembly>
+ PUSHW[ ] /* 1 value pushed */
+ 511
+ SCANCTRL[ ] /* ScanConversionControl */
+ PUSHB[ ] /* 1 value pushed */
+ 4
+ SCANTYPE[ ] /* ScanType */
+ </assembly>
+ </prep>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef"/><!-- contains no outline data -->
+
+ <TTGlyph name="I" xMin="44" yMin="0" xMax="92" yMax="364">
+ <contour>
+ <pt x="44" y="0" on="1" overlap="1"/>
+ <pt x="92" y="0" on="1"/>
+ <pt x="92" y="364" on="1"/>
+ <pt x="44" y="364" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright 2017 The Roboto Flex Project Authors (https://github.com/TypeNetwork/Roboto-Flex)
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Roboto Flex
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ Google:Roboto Regular:2017
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Roboto Flex Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 3.100
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlex-Regular
+ </namerecord>
+ <namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
+ wght
+ </namerecord>
+ <namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
+ wdth
+ </namerecord>
+ <namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
+ opsz
+ </namerecord>
+ <namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
+ GRAD
+ </namerecord>
+ <namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
+ slnt
+ </namerecord>
+ <namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
+ XTRA
+ </namerecord>
+ <namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
+ XOPQ
+ </namerecord>
+ <namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
+ YOPQ
+ </namerecord>
+ <namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
+ YTLC
+ </namerecord>
+ <namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
+ YTUC
+ </namerecord>
+ <namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
+ YTAS
+ </namerecord>
+ <namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
+ YTDE
+ </namerecord>
+ <namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
+ YTFI
+ </namerecord>
+ <namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
+ Thin
+ </namerecord>
+ <namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
+ ExtraLight
+ </namerecord>
+ <namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
+ Light
+ </namerecord>
+ <namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
+ Medium
+ </namerecord>
+ <namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
+ SemiBold
+ </namerecord>
+ <namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
+ Bold
+ </namerecord>
+ <namerecord nameID="276" platformID="3" platEncID="1" langID="0x409">
+ ExtraBold
+ </namerecord>
+ <namerecord nameID="277" platformID="3" platEncID="1" langID="0x409">
+ Black
+ </namerecord>
+ <namerecord nameID="278" platformID="3" platEncID="1" langID="0x409">
+ ExtraBlack
+ </namerecord>
+ <namerecord nameID="279" platformID="3" platEncID="1" langID="0x409">
+ Thin Italic
+ </namerecord>
+ <namerecord nameID="280" platformID="3" platEncID="1" langID="0x409">
+ ExtraLight Italic
+ </namerecord>
+ <namerecord nameID="281" platformID="3" platEncID="1" langID="0x409">
+ Light Italic
+ </namerecord>
+ <namerecord nameID="282" platformID="3" platEncID="1" langID="0x409">
+ Italic
+ </namerecord>
+ <namerecord nameID="283" platformID="3" platEncID="1" langID="0x409">
+ Medium Italic
+ </namerecord>
+ <namerecord nameID="284" platformID="3" platEncID="1" langID="0x409">
+ SemiBold Italic
+ </namerecord>
+ <namerecord nameID="285" platformID="3" platEncID="1" langID="0x409">
+ Bold Italic
+ </namerecord>
+ <namerecord nameID="286" platformID="3" platEncID="1" langID="0x409">
+ ExtraBold Italic
+ </namerecord>
+ <namerecord nameID="287" platformID="3" platEncID="1" langID="0x409">
+ Black Italic
+ </namerecord>
+ <namerecord nameID="288" platformID="3" platEncID="1" langID="0x409">
+ ExtraBlack Italic
+ </namerecord>
+ <namerecord nameID="289" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Thin
+ </namerecord>
+ <namerecord nameID="290" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLight
+ </namerecord>
+ <namerecord nameID="291" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Light
+ </namerecord>
+ <namerecord nameID="292" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Regular
+ </namerecord>
+ <namerecord nameID="293" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Medium
+ </namerecord>
+ <namerecord nameID="294" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBold
+ </namerecord>
+ <namerecord nameID="295" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Bold
+ </namerecord>
+ <namerecord nameID="296" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBold
+ </namerecord>
+ <namerecord nameID="297" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Black
+ </namerecord>
+ <namerecord nameID="298" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlack
+ </namerecord>
+ <namerecord nameID="299" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ThinItalic
+ </namerecord>
+ <namerecord nameID="300" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLightItalic
+ </namerecord>
+ <namerecord nameID="301" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-LightItalic
+ </namerecord>
+ <namerecord nameID="302" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Italic
+ </namerecord>
+ <namerecord nameID="303" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-MediumItalic
+ </namerecord>
+ <namerecord nameID="304" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBoldItalic
+ </namerecord>
+ <namerecord nameID="305" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BoldItalic
+ </namerecord>
+ <namerecord nameID="306" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBoldItalic
+ </namerecord>
+ <namerecord nameID="307" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BlackItalic
+ </namerecord>
+ <namerecord nameID="308" platformID="3" platEncID="1" langID="0x409">
+ RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlackItalic
+ </namerecord>
+ <namerecord nameID="309" platformID="3" platEncID="1" langID="0x409">
+ Grade
+ </namerecord>
+ <namerecord nameID="310" platformID="3" platEncID="1" langID="0x409">
+ Normal
+ </namerecord>
+ <namerecord nameID="311" platformID="3" platEncID="1" langID="0x409">
+ X opaque
+ </namerecord>
+ <namerecord nameID="312" platformID="3" platEncID="1" langID="0x409">
+ X transparent
+ </namerecord>
+ <namerecord nameID="313" platformID="3" platEncID="1" langID="0x409">
+ Y opaque
+ </namerecord>
+ <namerecord nameID="314" platformID="3" platEncID="1" langID="0x409">
+ Y transparent ascender
+ </namerecord>
+ <namerecord nameID="315" platformID="3" platEncID="1" langID="0x409">
+ Y transparent descender
+ </namerecord>
+ <namerecord nameID="316" platformID="3" platEncID="1" langID="0x409">
+ Y transparent figures
+ </namerecord>
+ <namerecord nameID="317" platformID="3" platEncID="1" langID="0x409">
+ Y transparent lowercase
+ </namerecord>
+ <namerecord nameID="318" platformID="3" platEncID="1" langID="0x409">
+ Y transparent uppercase
+ </namerecord>
+ <namerecord nameID="319" platformID="3" platEncID="1" langID="0x409">
+ Optical size
+ </namerecord>
+ <namerecord nameID="320" platformID="3" platEncID="1" langID="0x409">
+ 8pt
+ </namerecord>
+ <namerecord nameID="321" platformID="3" platEncID="1" langID="0x409">
+ 9pt
+ </namerecord>
+ <namerecord nameID="322" platformID="3" platEncID="1" langID="0x409">
+ 10pt
+ </namerecord>
+ <namerecord nameID="323" platformID="3" platEncID="1" langID="0x409">
+ 11pt
+ </namerecord>
+ <namerecord nameID="324" platformID="3" platEncID="1" langID="0x409">
+ 12pt
+ </namerecord>
+ <namerecord nameID="325" platformID="3" platEncID="1" langID="0x409">
+ 14pt
+ </namerecord>
+ <namerecord nameID="326" platformID="3" platEncID="1" langID="0x409">
+ 16pt
+ </namerecord>
+ <namerecord nameID="327" platformID="3" platEncID="1" langID="0x409">
+ 17pt
+ </namerecord>
+ <namerecord nameID="328" platformID="3" platEncID="1" langID="0x409">
+ 18pt
+ </namerecord>
+ <namerecord nameID="329" platformID="3" platEncID="1" langID="0x409">
+ 20pt
+ </namerecord>
+ <namerecord nameID="330" platformID="3" platEncID="1" langID="0x409">
+ 24pt
+ </namerecord>
+ <namerecord nameID="331" platformID="3" platEncID="1" langID="0x409">
+ 28pt
+ </namerecord>
+ <namerecord nameID="332" platformID="3" platEncID="1" langID="0x409">
+ 36pt
+ </namerecord>
+ <namerecord nameID="333" platformID="3" platEncID="1" langID="0x409">
+ 48pt
+ </namerecord>
+ <namerecord nameID="334" platformID="3" platEncID="1" langID="0x409">
+ 60pt
+ </namerecord>
+ <namerecord nameID="335" platformID="3" platEncID="1" langID="0x409">
+ 72pt
+ </namerecord>
+ <namerecord nameID="336" platformID="3" platEncID="1" langID="0x409">
+ 96pt
+ </namerecord>
+ <namerecord nameID="337" platformID="3" platEncID="1" langID="0x409">
+ 120pt
+ </namerecord>
+ <namerecord nameID="338" platformID="3" platEncID="1" langID="0x409">
+ 144pt
+ </namerecord>
+ <namerecord nameID="339" platformID="3" platEncID="1" langID="0x409">
+ Width
+ </namerecord>
+ <namerecord nameID="340" platformID="3" platEncID="1" langID="0x409">
+ UltraCondensed
+ </namerecord>
+ <namerecord nameID="341" platformID="3" platEncID="1" langID="0x409">
+ ExtraCondensed
+ </namerecord>
+ <namerecord nameID="342" platformID="3" platEncID="1" langID="0x409">
+ Condensed
+ </namerecord>
+ <namerecord nameID="343" platformID="3" platEncID="1" langID="0x409">
+ SemiCondensed
+ </namerecord>
+ <namerecord nameID="344" platformID="3" platEncID="1" langID="0x409">
+ SemiExpanded
+ </namerecord>
+ <namerecord nameID="345" platformID="3" platEncID="1" langID="0x409">
+ Expanded
+ </namerecord>
+ <namerecord nameID="346" platformID="3" platEncID="1" langID="0x409">
+ ExtraExpanded
+ </namerecord>
+ <namerecord nameID="347" platformID="3" platEncID="1" langID="0x409">
+ Weight
+ </namerecord>
+ <namerecord nameID="348" platformID="3" platEncID="1" langID="0x409">
+ Slant
+ </namerecord>
+ <namerecord nameID="349" platformID="3" platEncID="1" langID="0x409">
+ Default
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-46"/>
+ <underlineThickness value="42"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ </post>
+
+ <gasp>
+ <gaspRange rangeMaxPPEM="65535" rangeGaspBehavior="15"/>
+ </gasp>
+
+ <GDEF>
+ <Version value="0x00010002"/>
+ <GlyphClassDef>
+ <ClassDef glyph="I" class="1"/>
+ </GlyphClassDef>
+ <MarkGlyphSetsDef>
+ <MarkSetTableFormat value="1"/>
+ <!-- MarkSetCount=3 -->
+ <Coverage index="0">
+ </Coverage>
+ <Coverage index="1">
+ </Coverage>
+ <Coverage index="2">
+ </Coverage>
+ </MarkGlyphSetsDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=1 -->
+ <FeatureRecord index="0">
+ <FeatureTag value="kern"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="2"/>
+ <LookupFlag value="8"/><!-- ignoreMarks -->
+ <!-- SubTableCount=1 -->
+ <PairPos index="0" Format="2">
+ <Coverage>
+ <Glyph value="I"/>
+ </Coverage>
+ <ValueFormat1 value="68"/>
+ <ValueFormat2 value="0"/>
+ <ClassDef1>
+ </ClassDef1>
+ <ClassDef2>
+ <ClassDef glyph="I" class="1"/>
+ </ClassDef2>
+ <!-- Class1Count=1 -->
+ <!-- Class2Count=2 -->
+ <Class1Record index="0">
+ <Class2Record index="0">
+ <Value1 XAdvance="0"/>
+ </Class2Record>
+ <Class2Record index="1">
+ <Value1 XAdvance="0"/>
+ </Class2Record>
+ </Class1Record>
+ </PairPos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=2 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=0 -->
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="1">
+ <ScriptTag value="latn"/>
+ <Script>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=0 -->
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=0 -->
+ </LookupList>
+ </GSUB>
+
+ <HVAR>
+ <Version value="0x00010000"/>
+ <VarStore Format="1">
+ <Format value="1"/>
+ <VarRegionList>
+ <!-- RegionAxisCount=13 -->
+ <!-- RegionCount=25 -->
+ <Region index="0">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="1">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="2">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="3">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="4">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="5">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="6">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="7">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="8">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="9">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="10">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="11">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="12">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="13">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="14">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="15">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="16">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="17">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="18">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="19">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="20">
+ <VarRegionAxis index="0">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="21">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="22">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="23">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="-1.0"/>
+ <PeakCoord value="-1.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ <Region index="24">
+ <VarRegionAxis index="0">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="1">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="2">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="1.0"/>
+ <EndCoord value="1.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="3">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="4">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="5">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="6">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="7">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="8">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="9">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="10">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="11">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ <VarRegionAxis index="12">
+ <StartCoord value="0.0"/>
+ <PeakCoord value="0.0"/>
+ <EndCoord value="0.0"/>
+ </VarRegionAxis>
+ </Region>
+ </VarRegionList>
+ <!-- VarDataCount=2 -->
+ <VarData index="0">
+ <!-- ItemCount=1 -->
+ <NumShorts value="0"/>
+ <!-- VarRegionCount=0 -->
+ <Item index="0" value="[]"/>
+ </VarData>
+ <VarData index="1">
+ <!-- ItemCount=1 -->
+ <NumShorts value="7"/>
+ <!-- VarRegionCount=25 -->
+ <VarRegionIndex index="0" value="1"/>
+ <VarRegionIndex index="1" value="5"/>
+ <VarRegionIndex index="2" value="6"/>
+ <VarRegionIndex index="3" value="7"/>
+ <VarRegionIndex index="4" value="14"/>
+ <VarRegionIndex index="5" value="15"/>
+ <VarRegionIndex index="6" value="17"/>
+ <VarRegionIndex index="7" value="0"/>
+ <VarRegionIndex index="8" value="2"/>
+ <VarRegionIndex index="9" value="3"/>
+ <VarRegionIndex index="10" value="4"/>
+ <VarRegionIndex index="11" value="9"/>
+ <VarRegionIndex index="12" value="11"/>
+ <VarRegionIndex index="13" value="12"/>
+ <VarRegionIndex index="14" value="13"/>
+ <VarRegionIndex index="15" value="16"/>
+ <VarRegionIndex index="16" value="18"/>
+ <VarRegionIndex index="17" value="19"/>
+ <VarRegionIndex index="18" value="20"/>
+ <VarRegionIndex index="19" value="21"/>
+ <VarRegionIndex index="20" value="22"/>
+ <VarRegionIndex index="21" value="23"/>
+ <VarRegionIndex index="22" value="24"/>
+ <VarRegionIndex index="23" value="8"/>
+ <VarRegionIndex index="24" value="10"/>
+ <Item index="0" value="[34, -38, -34, 40, 43, -47, 43, -24, -2, 3, 3, 6, 0, 6, 2, 22, 0, -2, 4, -12, -1, 4, -8, 0, 8]"/>
+ </VarData>
+ </VarStore>
+ <AdvWidthMap>
+ <Map glyph=".notdef" outer="0" inner="0"/>
+ <Map glyph="I" outer="1" inner="0"/>
+ </AdvWidthMap>
+ </HVAR>
+
+ <STAT>
+ <Version value="0x00010001"/>
+ <DesignAxisRecordSize value="8"/>
+ <!-- DesignAxisCount=13 -->
+ <DesignAxisRecord>
+ <Axis index="0">
+ <AxisTag value="GRAD"/>
+ <AxisNameID value="309"/> <!-- Grade -->
+ <AxisOrdering value="0"/>
+ </Axis>
+ <Axis index="1">
+ <AxisTag value="XOPQ"/>
+ <AxisNameID value="311"/> <!-- X opaque -->
+ <AxisOrdering value="1"/>
+ </Axis>
+ <Axis index="2">
+ <AxisTag value="XTRA"/>
+ <AxisNameID value="312"/> <!-- X transparent -->
+ <AxisOrdering value="2"/>
+ </Axis>
+ <Axis index="3">
+ <AxisTag value="YOPQ"/>
+ <AxisNameID value="313"/> <!-- Y opaque -->
+ <AxisOrdering value="3"/>
+ </Axis>
+ <Axis index="4">
+ <AxisTag value="YTAS"/>
+ <AxisNameID value="314"/> <!-- Y transparent ascender -->
+ <AxisOrdering value="4"/>
+ </Axis>
+ <Axis index="5">
+ <AxisTag value="YTDE"/>
+ <AxisNameID value="315"/> <!-- Y transparent descender -->
+ <AxisOrdering value="5"/>
+ </Axis>
+ <Axis index="6">
+ <AxisTag value="YTFI"/>
+ <AxisNameID value="316"/> <!-- Y transparent figures -->
+ <AxisOrdering value="6"/>
+ </Axis>
+ <Axis index="7">
+ <AxisTag value="YTLC"/>
+ <AxisNameID value="317"/> <!-- Y transparent lowercase -->
+ <AxisOrdering value="7"/>
+ </Axis>
+ <Axis index="8">
+ <AxisTag value="YTUC"/>
+ <AxisNameID value="318"/> <!-- Y transparent uppercase -->
+ <AxisOrdering value="8"/>
+ </Axis>
+ <Axis index="9">
+ <AxisTag value="opsz"/>
+ <AxisNameID value="319"/> <!-- Optical size -->
+ <AxisOrdering value="9"/>
+ </Axis>
+ <Axis index="10">
+ <AxisTag value="wdth"/>
+ <AxisNameID value="339"/> <!-- Width -->
+ <AxisOrdering value="10"/>
+ </Axis>
+ <Axis index="11">
+ <AxisTag value="wght"/>
+ <AxisNameID value="347"/> <!-- Weight -->
+ <AxisOrdering value="11"/>
+ </Axis>
+ <Axis index="12">
+ <AxisTag value="slnt"/>
+ <AxisNameID value="348"/> <!-- Slant -->
+ <AxisOrdering value="12"/>
+ </Axis>
+ </DesignAxisRecord>
+ <!-- AxisValueCount=46 -->
+ <AxisValueArray>
+ <AxisValue index="0" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="0.0"/>
+ </AxisValue>
+ <AxisValue index="1" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="88.0"/>
+ </AxisValue>
+ <AxisValue index="2" Format="1">
+ <AxisIndex value="2"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="400.0"/>
+ </AxisValue>
+ <AxisValue index="3" Format="1">
+ <AxisIndex value="3"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="116.0"/>
+ </AxisValue>
+ <AxisValue index="4" Format="1">
+ <AxisIndex value="4"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="750.0"/>
+ </AxisValue>
+ <AxisValue index="5" Format="1">
+ <AxisIndex value="5"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="-250.0"/>
+ </AxisValue>
+ <AxisValue index="6" Format="1">
+ <AxisIndex value="6"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="600.0"/>
+ </AxisValue>
+ <AxisValue index="7" Format="1">
+ <AxisIndex value="7"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="500.0"/>
+ </AxisValue>
+ <AxisValue index="8" Format="1">
+ <AxisIndex value="8"/>
+ <Flags value="0"/>
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="725.0"/>
+ </AxisValue>
+ <AxisValue index="9" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="320"/> <!-- 8pt -->
+ <Value value="8.0"/>
+ </AxisValue>
+ <AxisValue index="10" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="321"/> <!-- 9pt -->
+ <Value value="9.0"/>
+ </AxisValue>
+ <AxisValue index="11" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="322"/> <!-- 10pt -->
+ <Value value="10.0"/>
+ </AxisValue>
+ <AxisValue index="12" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="323"/> <!-- 11pt -->
+ <Value value="11.0"/>
+ </AxisValue>
+ <AxisValue index="13" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="324"/> <!-- 12pt -->
+ <Value value="12.0"/>
+ </AxisValue>
+ <AxisValue index="14" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="325"/> <!-- 14pt -->
+ <Value value="14.0"/>
+ </AxisValue>
+ <AxisValue index="15" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="326"/> <!-- 16pt -->
+ <Value value="16.0"/>
+ </AxisValue>
+ <AxisValue index="16" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="327"/> <!-- 17pt -->
+ <Value value="17.0"/>
+ </AxisValue>
+ <AxisValue index="17" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="328"/> <!-- 18pt -->
+ <Value value="18.0"/>
+ </AxisValue>
+ <AxisValue index="18" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="329"/> <!-- 20pt -->
+ <Value value="20.0"/>
+ </AxisValue>
+ <AxisValue index="19" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="330"/> <!-- 24pt -->
+ <Value value="24.0"/>
+ </AxisValue>
+ <AxisValue index="20" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="331"/> <!-- 28pt -->
+ <Value value="28.0"/>
+ </AxisValue>
+ <AxisValue index="21" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="332"/> <!-- 36pt -->
+ <Value value="36.0"/>
+ </AxisValue>
+ <AxisValue index="22" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="333"/> <!-- 48pt -->
+ <Value value="48.0"/>
+ </AxisValue>
+ <AxisValue index="23" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="334"/> <!-- 60pt -->
+ <Value value="60.0"/>
+ </AxisValue>
+ <AxisValue index="24" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="335"/> <!-- 72pt -->
+ <Value value="72.0"/>
+ </AxisValue>
+ <AxisValue index="25" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="336"/> <!-- 96pt -->
+ <Value value="96.0"/>
+ </AxisValue>
+ <AxisValue index="26" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="337"/> <!-- 120pt -->
+ <Value value="120.0"/>
+ </AxisValue>
+ <AxisValue index="27" Format="1">
+ <AxisIndex value="9"/>
+ <Flags value="0"/>
+ <ValueNameID value="338"/> <!-- 144pt -->
+ <Value value="144.0"/>
+ </AxisValue>
+ <AxisValue index="28" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="340"/> <!-- UltraCondensed -->
+ <Value value="50.0"/>
+ </AxisValue>
+ <AxisValue index="29" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="341"/> <!-- ExtraCondensed -->
+ <Value value="62.5"/>
+ </AxisValue>
+ <AxisValue index="30" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="342"/> <!-- Condensed -->
+ <Value value="75.0"/>
+ </AxisValue>
+ <AxisValue index="31" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="343"/> <!-- SemiCondensed -->
+ <Value value="87.5"/>
+ </AxisValue>
+ <AxisValue index="32" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="310"/> <!-- Normal -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="33" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="344"/> <!-- SemiExpanded -->
+ <Value value="112.5"/>
+ </AxisValue>
+ <AxisValue index="34" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="345"/> <!-- Expanded -->
+ <Value value="125.0"/>
+ </AxisValue>
+ <AxisValue index="35" Format="1">
+ <AxisIndex value="10"/>
+ <Flags value="0"/>
+ <ValueNameID value="346"/> <!-- ExtraExpanded -->
+ <Value value="150.0"/>
+ </AxisValue>
+ <AxisValue index="36" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="269"/> <!-- Thin -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="37" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="270"/> <!-- ExtraLight -->
+ <Value value="200.0"/>
+ </AxisValue>
+ <AxisValue index="38" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="271"/> <!-- Light -->
+ <Value value="300.0"/>
+ </AxisValue>
+ <AxisValue index="39" Format="3">
+ <AxisIndex value="11"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="272"/> <!-- Regular -->
+ <Value value="400.0"/>
+ <LinkedValue value="700.0"/>
+ </AxisValue>
+ <AxisValue index="40" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="273"/> <!-- Medium -->
+ <Value value="500.0"/>
+ </AxisValue>
+ <AxisValue index="41" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="274"/> <!-- SemiBold -->
+ <Value value="600.0"/>
+ </AxisValue>
+ <AxisValue index="42" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="275"/> <!-- Bold -->
+ <Value value="700.0"/>
+ </AxisValue>
+ <AxisValue index="43" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="276"/> <!-- ExtraBold -->
+ <Value value="800.0"/>
+ </AxisValue>
+ <AxisValue index="44" Format="1">
+ <AxisIndex value="11"/>
+ <Flags value="0"/>
+ <ValueNameID value="277"/> <!-- Black -->
+ <Value value="900.0"/>
+ </AxisValue>
+ <AxisValue index="45" Format="1">
+ <AxisIndex value="12"/>
+ <Flags value="2"/> <!-- ElidableAxisValueName -->
+ <ValueNameID value="349"/> <!-- Default -->
+ <Value value="0.0"/>
+ </AxisValue>
+ </AxisValueArray>
+ <ElidedFallbackNameID value="2"/> <!-- Regular -->
+ </STAT>
+
+ <avar>
+ <segment axis="wght">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="wdth">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="opsz">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="0.16925" to="0.492"/>
+ <mapping from="0.53845" to="0.946"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="GRAD">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="slnt">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="XTRA">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="XOPQ">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YOPQ">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTLC">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTUC">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTAS">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTDE">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ <segment axis="YTFI">
+ <mapping from="-1.0" to="-1.0"/>
+ <mapping from="0.0" to="0.0"/>
+ <mapping from="1.0" to="1.0"/>
+ </segment>
+ </avar>
+
+ <fvar>
+
+ <!-- wght -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>100.0</MinValue>
+ <DefaultValue>400.0</DefaultValue>
+ <MaxValue>1000.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+
+ <!-- wdth -->
+ <Axis>
+ <AxisTag>wdth</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>25.0</MinValue>
+ <DefaultValue>100.0</DefaultValue>
+ <MaxValue>151.0</MaxValue>
+ <AxisNameID>257</AxisNameID>
+ </Axis>
+
+ <!-- opsz -->
+ <Axis>
+ <AxisTag>opsz</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>8.0</MinValue>
+ <DefaultValue>14.0</DefaultValue>
+ <MaxValue>144.0</MaxValue>
+ <AxisNameID>258</AxisNameID>
+ </Axis>
+
+ <!-- GRAD -->
+ <Axis>
+ <AxisTag>GRAD</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>-200.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>150.0</MaxValue>
+ <AxisNameID>259</AxisNameID>
+ </Axis>
+
+ <!-- slnt -->
+ <Axis>
+ <AxisTag>slnt</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>-10.0</MinValue>
+ <DefaultValue>0.0</DefaultValue>
+ <MaxValue>0.0</MaxValue>
+ <AxisNameID>260</AxisNameID>
+ </Axis>
+
+ <!-- XTRA -->
+ <Axis>
+ <AxisTag>XTRA</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>323.0</MinValue>
+ <DefaultValue>468.0</DefaultValue>
+ <MaxValue>603.0</MaxValue>
+ <AxisNameID>261</AxisNameID>
+ </Axis>
+
+ <!-- XOPQ -->
+ <Axis>
+ <AxisTag>XOPQ</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>27.0</MinValue>
+ <DefaultValue>96.0</DefaultValue>
+ <MaxValue>175.0</MaxValue>
+ <AxisNameID>262</AxisNameID>
+ </Axis>
+
+ <!-- YOPQ -->
+ <Axis>
+ <AxisTag>YOPQ</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>25.0</MinValue>
+ <DefaultValue>79.0</DefaultValue>
+ <MaxValue>135.0</MaxValue>
+ <AxisNameID>263</AxisNameID>
+ </Axis>
+
+ <!-- YTLC -->
+ <Axis>
+ <AxisTag>YTLC</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>416.0</MinValue>
+ <DefaultValue>514.0</DefaultValue>
+ <MaxValue>570.0</MaxValue>
+ <AxisNameID>264</AxisNameID>
+ </Axis>
+
+ <!-- YTUC -->
+ <Axis>
+ <AxisTag>YTUC</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>528.0</MinValue>
+ <DefaultValue>712.0</DefaultValue>
+ <MaxValue>760.0</MaxValue>
+ <AxisNameID>265</AxisNameID>
+ </Axis>
+
+ <!-- YTAS -->
+ <Axis>
+ <AxisTag>YTAS</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>649.0</MinValue>
+ <DefaultValue>750.0</DefaultValue>
+ <MaxValue>854.0</MaxValue>
+ <AxisNameID>266</AxisNameID>
+ </Axis>
+
+ <!-- YTDE -->
+ <Axis>
+ <AxisTag>YTDE</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>-305.0</MinValue>
+ <DefaultValue>-203.0</DefaultValue>
+ <MaxValue>-98.0</MaxValue>
+ <AxisNameID>267</AxisNameID>
+ </Axis>
+
+ <!-- YTFI -->
+ <Axis>
+ <AxisTag>YTFI</AxisTag>
+ <Flags>0x1</Flags>
+ <MinValue>560.0</MinValue>
+ <DefaultValue>738.0</DefaultValue>
+ <MaxValue>788.0</MaxValue>
+ <AxisNameID>268</AxisNameID>
+ </Axis>
+
+ <!-- Thin -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Thin -->
+ <NamedInstance flags="0x0" postscriptNameID="289" subfamilyNameID="269">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraLight -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLight -->
+ <NamedInstance flags="0x0" postscriptNameID="290" subfamilyNameID="270">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Light -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Light -->
+ <NamedInstance flags="0x0" postscriptNameID="291" subfamilyNameID="271">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Regular -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Regular -->
+ <NamedInstance flags="0x0" postscriptNameID="292" subfamilyNameID="272">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Medium -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Medium -->
+ <NamedInstance flags="0x0" postscriptNameID="293" subfamilyNameID="273">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- SemiBold -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBold -->
+ <NamedInstance flags="0x0" postscriptNameID="294" subfamilyNameID="274">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Bold -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Bold -->
+ <NamedInstance flags="0x0" postscriptNameID="295" subfamilyNameID="275">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBold -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBold -->
+ <NamedInstance flags="0x0" postscriptNameID="296" subfamilyNameID="276">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Black -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Black -->
+ <NamedInstance flags="0x0" postscriptNameID="297" subfamilyNameID="277">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBlack -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlack -->
+ <NamedInstance flags="0x0" postscriptNameID="298" subfamilyNameID="278">
+ <coord axis="wght" value="1000.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="0.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Thin Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ThinItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="299" subfamilyNameID="279">
+ <coord axis="wght" value="100.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraLight Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraLightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="300" subfamilyNameID="280">
+ <coord axis="wght" value="200.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Light Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-LightItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="301" subfamilyNameID="281">
+ <coord axis="wght" value="300.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-Italic -->
+ <NamedInstance flags="0x0" postscriptNameID="302" subfamilyNameID="282">
+ <coord axis="wght" value="400.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Medium Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-MediumItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="303" subfamilyNameID="283">
+ <coord axis="wght" value="500.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- SemiBold Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-SemiBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="304" subfamilyNameID="284">
+ <coord axis="wght" value="600.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Bold Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="305" subfamilyNameID="285">
+ <coord axis="wght" value="700.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBold Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBoldItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="306" subfamilyNameID="286">
+ <coord axis="wght" value="800.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- Black Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-BlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="307" subfamilyNameID="287">
+ <coord axis="wght" value="900.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+
+ <!-- ExtraBlack Italic -->
+ <!-- PostScript: RobotoFlexNormalNormalNormalNormalNormalNormalNormalNormalNormalDefault-ExtraBlackItalic -->
+ <NamedInstance flags="0x0" postscriptNameID="308" subfamilyNameID="288">
+ <coord axis="wght" value="1000.0"/>
+ <coord axis="wdth" value="100.0"/>
+ <coord axis="opsz" value="14.0"/>
+ <coord axis="GRAD" value="0.0"/>
+ <coord axis="slnt" value="-10.0"/>
+ <coord axis="XTRA" value="468.0"/>
+ <coord axis="XOPQ" value="96.0"/>
+ <coord axis="YOPQ" value="79.0"/>
+ <coord axis="YTLC" value="514.0"/>
+ <coord axis="YTUC" value="712.0"/>
+ <coord axis="YTAS" value="750.0"/>
+ <coord axis="YTDE" value="-203.0"/>
+ <coord axis="YTFI" value="738.0"/>
+ </NamedInstance>
+ </fvar>
+
+ <gvar>
+ <version value="1"/>
+ <reserved value="0"/>
+ <glyphVariations glyph="I">
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="-24" y="0"/>
+ <delta pt="2" x="-24" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-24" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="-12" y="0"/>
+ <delta pt="2" x="46" y="0"/>
+ <delta pt="5" x="34" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="-2" y="0"/>
+ <delta pt="2" x="-2" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-2" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="3" y="0"/>
+ <delta pt="2" x="3" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="3" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="3" y="0"/>
+ <delta pt="2" x="3" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="3" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-9" y="0"/>
+ <delta pt="1" x="-29" y="0"/>
+ <delta pt="2" x="-29" y="0"/>
+ <delta pt="3" x="-9" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-38" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="8" y="0"/>
+ <delta pt="1" x="-8" y="0"/>
+ <delta pt="2" x="-8" y="0"/>
+ <delta pt="3" x="8" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="-8" y="0"/>
+ <delta pt="1" x="8" y="0"/>
+ <delta pt="2" x="8" y="0"/>
+ <delta pt="3" x="-8" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="-32" y="0"/>
+ <delta pt="1" x="-32" y="0"/>
+ <delta pt="2" x="32" y="0"/>
+ <delta pt="3" x="32" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="XOPQ" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="2" x="-34" y="0"/>
+ <delta pt="5" x="-34" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="XOPQ" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="2" x="40" y="0"/>
+ <delta pt="5" x="40" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="YTUC" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="-93"/>
+ <delta pt="3" x="0" y="-93"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="YTUC" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="25"/>
+ <delta pt="3" x="0" y="25"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <delta pt="0" x="3" y="0"/>
+ <delta pt="5" x="6" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <delta pt="0" x="3" y="0"/>
+ <delta pt="1" x="5" y="0"/>
+ <delta pt="2" x="5" y="0"/>
+ <delta pt="3" x="3" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="8" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="4" y="0"/>
+ <delta pt="1" x="3" y="0"/>
+ <delta pt="2" x="3" y="0"/>
+ <delta pt="3" x="4" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="6" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="11" y="0"/>
+ <delta pt="1" x="-9" y="0"/>
+ <delta pt="2" x="-9" y="0"/>
+ <delta pt="3" x="11" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="2" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-12" y="0"/>
+ <delta pt="2" x="55" y="0"/>
+ <delta pt="5" x="43" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="-5" y="0"/>
+ <delta pt="1" x="6" y="0"/>
+ <delta pt="2" x="6" y="0"/>
+ <delta pt="3" x="-5" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="6" y="0"/>
+ <delta pt="1" x="-6" y="0"/>
+ <delta pt="2" x="-6" y="0"/>
+ <delta pt="3" x="6" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="6" y="0"/>
+ <delta pt="1" x="-6" y="0"/>
+ <delta pt="2" x="-6" y="0"/>
+ <delta pt="3" x="6" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-23" y="0"/>
+ <delta pt="1" x="-24" y="0"/>
+ <delta pt="2" x="-24" y="0"/>
+ <delta pt="3" x="-23" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-47" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="11" y="0"/>
+ <delta pt="1" x="11" y="0"/>
+ <delta pt="2" x="11" y="0"/>
+ <delta pt="3" x="11" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="22" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="23" y="0"/>
+ <delta pt="1" x="20" y="0"/>
+ <delta pt="2" x="20" y="0"/>
+ <delta pt="3" x="23" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="43" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="-2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-2" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="2" y="0"/>
+ <delta pt="1" x="2" y="0"/>
+ <delta pt="2" x="2" y="0"/>
+ <delta pt="3" x="2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="4" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="-10" y="0"/>
+ <delta pt="2" x="-10" y="0"/>
+ <delta pt="3" x="-2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-12" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="12" y="0"/>
+ <delta pt="1" x="-13" y="0"/>
+ <delta pt="2" x="-13" y="0"/>
+ <delta pt="3" x="12" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-1" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <delta pt="0" x="-5" y="0"/>
+ <delta pt="1" x="9" y="0"/>
+ <delta pt="2" x="9" y="0"/>
+ <delta pt="3" x="-5" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="4" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <delta pt="0" x="-3" y="0"/>
+ <delta pt="1" x="-5" y="0"/>
+ <delta pt="2" x="-5" y="0"/>
+ <delta pt="3" x="-3" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="-8" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="2" y="0"/>
+ <delta pt="2" x="2" y="0"/>
+ <delta pt="3" x="-2" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="1" y="0"/>
+ <delta pt="1" x="-1" y="0"/>
+ <delta pt="2" x="-1" y="0"/>
+ <delta pt="3" x="1" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="-5" y="0"/>
+ <delta pt="1" x="6" y="0"/>
+ <delta pt="2" x="6" y="0"/>
+ <delta pt="3" x="-5" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="1" y="0"/>
+ <delta pt="1" x="-1" y="0"/>
+ <delta pt="2" x="-1" y="0"/>
+ <delta pt="3" x="1" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="1.0"/>
+ <coord axis="GRAD" value="1.0"/>
+ <delta pt="0" x="1" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="1" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="-1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="wdth" value="1.0"/>
+ <coord axis="opsz" value="-1.0"/>
+ <coord axis="slnt" value="-1.0"/>
+ <delta pt="0" x="0" y="0"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="0" y="0"/>
+ <delta pt="3" x="0" y="0"/>
+ <delta pt="4" x="0" y="0"/>
+ <delta pt="5" x="0" y="0"/>
+ <delta pt="6" x="0" y="0"/>
+ <delta pt="7" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ </gvar>
+
+</ttFont>
diff --git a/Tests/ttLib/data/I.ttf b/Tests/ttLib/data/I.ttf
new file mode 100644
index 00000000..119c24c8
--- /dev/null
+++ b/Tests/ttLib/data/I.ttf
Binary files differ
diff --git a/Tests/ttLib/scaleUpem_test.py b/Tests/ttLib/scaleUpem_test.py
new file mode 100644
index 00000000..dc52bf94
--- /dev/null
+++ b/Tests/ttLib/scaleUpem_test.py
@@ -0,0 +1,75 @@
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.scaleUpem import scale_upem
+import difflib
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+import pytest
+
+class ScaleUpemTest(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = None
+ self.num_tempfiles = 0
+
+ def tearDown(self):
+ if self.tempdir:
+ shutil.rmtree(self.tempdir)
+
+ @staticmethod
+ def get_path(test_file_or_folder):
+ parent_dir = os.path.dirname(__file__)
+ return os.path.join(parent_dir, "data", test_file_or_folder)
+
+ def temp_path(self, suffix):
+ self.temp_dir()
+ self.num_tempfiles += 1
+ return os.path.join(self.tempdir,
+ "tmp%d%s" % (self.num_tempfiles, suffix))
+
+ def temp_dir(self):
+ if not self.tempdir:
+ self.tempdir = tempfile.mkdtemp()
+
+ def read_ttx(self, path):
+ lines = []
+ with open(path, "r", encoding="utf-8") as ttx:
+ for line in ttx.readlines():
+ # Elide ttFont attributes because ttLibVersion may change.
+ if line.startswith("<ttFont "):
+ lines.append("<ttFont>\n")
+ else:
+ lines.append(line.rstrip() + "\n")
+ return lines
+
+ def expect_ttx(self, font, expected_ttx, tables):
+ path = self.temp_path(suffix=".ttx")
+ font.saveXML(path, tables=tables)
+ actual = self.read_ttx(path)
+ expected = self.read_ttx(expected_ttx)
+ if actual != expected:
+ for line in difflib.unified_diff(
+ expected, actual, fromfile=expected_ttx, tofile=path):
+ sys.stdout.write(line)
+ self.fail("TTX output is different from expected")
+
+ def test_scale_upem_ttf(self):
+
+ font = TTFont(self.get_path("I.ttf"))
+ tables = [table_tag for table_tag in font.keys() if table_tag != "head"]
+
+ scale_upem(font, 512)
+
+ expected_ttx_path = self.get_path("I-512upem.ttx")
+ self.expect_ttx(font, expected_ttx_path, tables)
+
+
+ def test_scale_upem_otf(self):
+
+ # Just test that it doesn't crash
+
+ font = TTFont(self.get_path("TestVGID-Regular.otf"))
+
+ scale_upem(font, 500)
diff --git a/Tests/ttLib/tables/C_O_L_R_test.py b/Tests/ttLib/tables/C_O_L_R_test.py
index aaf33003..132449ea 100644
--- a/Tests/ttLib/tables/C_O_L_R_test.py
+++ b/Tests/ttLib/tables/C_O_L_R_test.py
@@ -400,8 +400,8 @@ COLR_V1_XML = [
" </ColorLine>",
' <centerX value="259"/>',
' <centerY value="300"/>',
- ' <startAngle value="45.0"/>',
- ' <endAngle value="135.0"/>',
+ ' <startAngle value="225.0"/>',
+ ' <endAngle value="315.0"/>',
" </Paint>",
' <Glyph value="glyph00011"/>',
" </Paint>",
@@ -532,17 +532,18 @@ COLR_V1_XML = [
COLR_V1_VAR_XML = [
'<VarIndexMap Format="0">',
- ' <Map index="0" outer="1" inner="1"/>',
- ' <Map index="1" outer="1" inner="0"/>',
- ' <Map index="2" outer="1" inner="0"/>',
- ' <Map index="3" outer="1" inner="1"/>',
- ' <Map index="4" outer="1" inner="0"/>',
- ' <Map index="5" outer="1" inner="0"/>',
+ ' <!-- Omitted values default to 0xFFFF/0xFFFF (no variations) -->',
+ ' <Map index="0" outer="1" inner="0"/>',
+ ' <Map index="1"/>',
+ ' <Map index="2"/>',
+ ' <Map index="3" outer="1" inner="0"/>',
+ ' <Map index="4"/>',
+ ' <Map index="5"/>',
' <Map index="6" outer="0" inner="2"/>',
' <Map index="7" outer="0" inner="0"/>',
' <Map index="8" outer="0" inner="1"/>',
- ' <Map index="9" outer="1" inner="0"/>',
- ' <Map index="10" outer="1" inner="0"/>',
+ ' <Map index="9"/>',
+ ' <Map index="10"/>',
' <Map index="11" outer="0" inner="3"/>',
' <Map index="12" outer="0" inner="3"/>',
"</VarIndexMap>",
@@ -571,12 +572,11 @@ COLR_V1_VAR_XML = [
' <Item index="3" value="[500]"/>',
" </VarData>",
' <VarData index="1">',
- " <!-- ItemCount=2 -->",
+ " <!-- ItemCount=1 -->",
' <NumShorts value="32769"/>',
" <!-- VarRegionCount=1 -->",
' <VarRegionIndex index="0" value="0"/>',
- ' <Item index="0" value="[0]"/>',
- ' <Item index="1" value="[65536]"/>',
+ ' <Item index="0" value="[65536]"/>',
" </VarData>",
"</VarStore>",
]
diff --git a/Tests/ttLib/tables/S_V_G__test.py b/Tests/ttLib/tables/S_V_G__test.py
index 91b0f23a..4c40556e 100644
--- a/Tests/ttLib/tables/S_V_G__test.py
+++ b/Tests/ttLib/tables/S_V_G__test.py
@@ -1,3 +1,5 @@
+import gzip
+import io
import struct
from fontTools.misc import etree
@@ -12,6 +14,13 @@ def dump(table, ttFont=None):
print("\n".join(getXML(table.toXML, ttFont)))
+def compress(data: bytes) -> bytes:
+ buf = io.BytesIO()
+ with gzip.GzipFile(None, "w", fileobj=buf, mtime=0) as gz:
+ gz.write(data)
+ return buf.getvalue()
+
+
def strip_xml_whitespace(xml_string):
def strip_or_none(text):
text = text.strip() if text else None
@@ -45,7 +54,9 @@ SVG_DOCS = [
b"""\
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<g id="glyph3">
- <path d="M0,0 L100,0 L50,100 Z"/>
+ <path d="M0,0 L100,0 L50,100 Z" fill="#red"/>
+ <path d="M10,10 L110,10 L60,110 Z" fill="#blue"/>
+ <path d="M20,20 L120,20 L70,120 Z" fill="#green"/>
</g>
</svg>""",
)
@@ -65,25 +76,25 @@ OTSVG_DATA = b"".join(
b"\x00\x02" # endGlyphID (2)
b"\x00\x00\x00\x26" # svgDocOffset (2 + 12*3 == 38 == 0x26)
+ struct.pack(">L", len(SVG_DOCS[0])) # svgDocLength
- # SVGDocumentRecord[1]
+ # SVGDocumentRecord[1] (compressed)
+ b"\x00\x03" # startGlyphID (3)
b"\x00\x03" # endGlyphID (3)
+ struct.pack(">L", 0x26 + len(SVG_DOCS[0])) # svgDocOffset
- + struct.pack(">L", len(SVG_DOCS[1])) # svgDocLength
+ + struct.pack(">L", len(compress(SVG_DOCS[1]))) # svgDocLength
# SVGDocumentRecord[2]
+ b"\x00\x04" # startGlyphID (4)
b"\x00\x04" # endGlyphID (4)
b"\x00\x00\x00\x26" # svgDocOffset (38); records 0 and 2 point to same SVG doc
+ struct.pack(">L", len(SVG_DOCS[0])) # svgDocLength
]
- + SVG_DOCS
+ + [SVG_DOCS[0], compress(SVG_DOCS[1])]
)
OTSVG_TTX = [
'<svgDoc endGlyphID="2" startGlyphID="1">',
f" <![CDATA[{SVG_DOCS[0].decode()}]]>",
"</svgDoc>",
- '<svgDoc endGlyphID="3" startGlyphID="3">',
+ '<svgDoc compressed="1" endGlyphID="3" startGlyphID="3">',
f" <![CDATA[{SVG_DOCS[1].decode()}]]>",
"</svgDoc>",
'<svgDoc endGlyphID="4" startGlyphID="4">',
@@ -129,3 +140,19 @@ def test_round_trip_ttx(font):
table = table_S_V_G_()
table.decompile(compiled, font)
assert getXML(table.toXML, font) == OTSVG_TTX
+
+
+def test_unpack_svg_doc_as_3_tuple():
+ # test that the legacy docList as list of 3-tuples interface still works
+ # even after the new SVGDocument class with extra `compressed` attribute
+ # was added
+ table = table_S_V_G_()
+ table.decompile(OTSVG_DATA, font)
+
+ for doc, compressed in zip(table.docList, (False, True, False)):
+ assert len(doc) == 3
+ data, startGID, endGID = doc
+ assert doc.data == data
+ assert doc.startGlyphID == startGID
+ assert doc.endGlyphID == endGID
+ assert doc.compressed == compressed
diff --git a/Tests/ttLib/ttGlyphSet_test.py b/Tests/ttLib/ttGlyphSet_test.py
new file mode 100644
index 00000000..bc0bf2ce
--- /dev/null
+++ b/Tests/ttLib/ttGlyphSet_test.py
@@ -0,0 +1,112 @@
+from fontTools.ttLib import TTFont
+from fontTools.ttLib import ttGlyphSet
+from fontTools.pens.recordingPen import RecordingPen
+import os
+import pytest
+
+
+class TTGlyphSetTest(object):
+
+ @staticmethod
+ def getpath(testfile):
+ path = os.path.dirname(__file__)
+ return os.path.join(path, "data", testfile)
+
+ @pytest.mark.parametrize(
+ "location, expected",
+ [
+ (
+ None,
+ [
+ ('moveTo', ((175, 0),)),
+ ('lineTo', ((367, 0),)),
+ ('lineTo', ((367, 1456),)),
+ ('lineTo', ((175, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {},
+ [
+ ('moveTo', ((175, 0),)),
+ ('lineTo', ((367, 0),)),
+ ('lineTo', ((367, 1456),)),
+ ('lineTo', ((175, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 100},
+ [
+ ('moveTo', ((175, 0),)),
+ ('lineTo', ((271, 0),)),
+ ('lineTo', ((271, 1456),)),
+ ('lineTo', ((175, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 1000},
+ [
+ ('moveTo', ((128, 0),)),
+ ('lineTo', ((550, 0),)),
+ ('lineTo', ((550, 1456),)),
+ ('lineTo', ((128, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 1000, 'wdth': 25},
+ [
+ ('moveTo', ((140, 0),)),
+ ('lineTo', ((553, 0),)),
+ ('lineTo', ((553, 1456),)),
+ ('lineTo', ((140, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ (
+ {'wght': 1000, 'wdth': 50},
+ [
+ ('moveTo', ((136, 0),)),
+ ('lineTo', ((552, 0),)),
+ ('lineTo', ((552, 1456),)),
+ ('lineTo', ((136, 1456),)),
+ ('closePath', ())
+ ]
+ ),
+ ]
+ )
+ def test_glyphset(
+ self, location, expected
+ ):
+ # TODO: also test loading CFF-flavored fonts
+ font = TTFont(self.getpath("I.ttf"))
+ glyphset = font.getGlyphSet(location=location)
+
+ assert isinstance(glyphset, ttGlyphSet._TTGlyphSet)
+ if location:
+ assert isinstance(glyphset, ttGlyphSet._TTVarGlyphSet)
+
+ assert list(glyphset.keys()) == [".notdef", "I"]
+
+ assert "I" in glyphset
+ assert glyphset.has_key("I") # we should really get rid of this...
+
+ assert len(glyphset) == 2
+
+ pen = RecordingPen()
+ glyph = glyphset['I']
+
+ assert glyphset.get("foobar") is None
+
+ assert isinstance(glyph, ttGlyphSet._TTGlyph)
+ if location:
+ assert isinstance(glyph, ttGlyphSet._TTVarGlyphGlyf)
+ else:
+ assert isinstance(glyph, ttGlyphSet._TTGlyphGlyf)
+
+ glyph.draw(pen)
+ actual = pen.value
+
+ assert actual == expected, (location, actual, expected)
diff --git a/Tests/ttLib/ttVisitor_test.py b/Tests/ttLib/ttVisitor_test.py
new file mode 100644
index 00000000..e84e213c
--- /dev/null
+++ b/Tests/ttLib/ttVisitor_test.py
@@ -0,0 +1,39 @@
+from fontTools.ttLib import TTFont
+from fontTools.ttLib.ttVisitor import TTVisitor
+import os
+import pytest
+
+
+class TestVisitor(TTVisitor):
+ def __init__(self):
+ self.value = []
+ self.depth = 0
+
+ def _add(self, s):
+ self.value.append(s)
+
+ def visit(self, obj, target_depth):
+ if self.depth == target_depth:
+ self._add(obj)
+ self.depth += 1
+ super().visit(obj, target_depth)
+ self.depth -= 1
+
+
+class TTVisitorTest(object):
+
+ @staticmethod
+ def getpath(testfile):
+ path = os.path.dirname(__file__)
+ return os.path.join(path, "data", testfile)
+
+ def test_ttvisitor(self):
+
+ font = TTFont(self.getpath("TestVGID-Regular.otf"))
+ visitor = TestVisitor()
+
+ # Count number of objects at depth 1:
+ # That is, number of font tables, including GlyphOrder.
+ visitor.visit(font, 1)
+
+ assert len(visitor.value) == 14
diff --git a/Tests/varLib/data/TestVariableCOLR.designspace b/Tests/varLib/data/TestVariableCOLR.designspace
new file mode 100644
index 00000000..0feddfab
--- /dev/null
+++ b/Tests/varLib/data/TestVariableCOLR.designspace
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<designspace format="5.0">
+ <axes>
+ <axis tag="wght" name="Weight" minimum="400" maximum="700" default="400"/>
+ </axes>
+ <sources>
+ <source filename="master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx" name="Regular">
+ <location>
+ <dimension name="Weight" xvalue="400"/>
+ </location>
+ </source>
+ <source filename="master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx" name="Bold">
+ <location>
+ <dimension name="Weight" xvalue="700"/>
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx
new file mode 100644
index 00000000..4202dabc
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Bold.ttx
@@ -0,0 +1,357 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".space"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="B"/>
+ <GlyphID id="4" name="A.0"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x92daf67f"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Tue Jul 5 12:29:44 2022"/>
+ <modified value="Tue Jul 5 12:29:44 2022"/>
+ <xMin value="51"/>
+ <yMin value="-250"/>
+ <xMax value="878"/>
+ <yMax value="950"/>
+ <macStyle value="00000000 00000001"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="1275"/>
+ <minLeftSideBearing value="51"/>
+ <minRightSideBearing value="397"/>
+ <xMaxExtent value="878"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="5"/>
+ <maxPoints value="16"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="1275"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="666"/>
+ <ySubscriptYSize value="614"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="77"/>
+ <ySuperscriptXSize value="666"/>
+ <ySuperscriptYSize value="614"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="358"/>
+ <yStrikeoutSize value="51"/>
+ <yStrikeoutPosition value="307"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 10100000"/>
+ <usFirstCharIndex value="32"/>
+ <usLastCharIndex value="66"/>
+ <sTypoAscender value="950"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="512"/>
+ <sCapHeight value="717"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="1275" lsb="51"/>
+ <mtx name=".space" width="1275" lsb="0"/>
+ <mtx name="A" width="1275" lsb="0"/>
+ <mtx name="A.0" width="1275" lsb="398"/>
+ <mtx name="B" width="1275" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
+ <contour>
+ <pt x="51" y="-250" on="1"/>
+ <pt x="51" y="950" on="1"/>
+ <pt x="461" y="950" on="1"/>
+ <pt x="461" y="-250" on="1"/>
+ </contour>
+ <contour>
+ <pt x="102" y="-199" on="1"/>
+ <pt x="410" y="-199" on="1"/>
+ <pt x="410" y="899" on="1"/>
+ <pt x="102" y="899" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name=".space"/><!-- contains no outline data -->
+
+ <TTGlyph name="A"/><!-- contains no outline data -->
+
+ <TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
+ <contour>
+ <pt x="878" y="350" on="1"/>
+ <pt x="878" y="416" on="0"/>
+ <pt x="813" y="525" on="0"/>
+ <pt x="704" y="590" on="0"/>
+ <pt x="638" y="590" on="1"/>
+ <pt x="571" y="590" on="0"/>
+ <pt x="462" y="525" on="0"/>
+ <pt x="398" y="416" on="0"/>
+ <pt x="398" y="350" on="1"/>
+ <pt x="398" y="284" on="0"/>
+ <pt x="462" y="175" on="0"/>
+ <pt x="571" y="110" on="0"/>
+ <pt x="638" y="110" on="1"/>
+ <pt x="704" y="110" on="0"/>
+ <pt x="813" y="175" on="0"/>
+ <pt x="878" y="284" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="B"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Bold
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.000;NONE;AnEmojiFamily-Bold
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family Bold
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ AnEmojiFamily-Bold
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-77"/>
+ <underlineThickness value="51"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name=".space"/>
+ <psName name="A.0"/>
+ </extraNames>
+ </post>
+
+ <COLR>
+ <Version value="1"/>
+ <!-- BaseGlyphRecordCount=0 -->
+ <!-- LayerRecordCount=0 -->
+ <BaseGlyphList>
+ <!-- BaseGlyphCount=2 -->
+ <BaseGlyphPaintRecord index="0">
+ <BaseGlyph value="A"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="3"/>
+ <FirstLayerIndex value="0"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ <BaseGlyphPaintRecord index="1">
+ <BaseGlyph value="B"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="2"/>
+ <FirstLayerIndex value="3"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ </BaseGlyphList>
+ <LayerList>
+ <!-- LayerCount=5 -->
+ <Paint index="0" Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="0"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <Paint index="1" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="100"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="2" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ <Paint index="3" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="0.5"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="4" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ </LayerList>
+ <ClipList Format="1">
+ <Clip>
+ <Glyph value="A"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="980"/>
+ <yMax value="600"/>
+ </ClipBox>
+ </Clip>
+ <Clip>
+ <Glyph value="B"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="480"/>
+ </ClipBox>
+ </Clip>
+ </ClipList>
+ </COLR>
+
+ <CPAL>
+ <version value="0"/>
+ <numPaletteEntries value="3"/>
+ <palette index="0">
+ <color index="0" value="#0000FFFF"/>
+ <color index="1" value="#008000FF"/>
+ <color index="2" value="#FF0000FF"/>
+ </palette>
+ </CPAL>
+
+</ttFont>
diff --git a/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx
new file mode 100644
index 00000000..195be50a
--- /dev/null
+++ b/Tests/varLib/data/master_ttx_varcolr_ttf/TestVariableCOLR-Regular.ttx
@@ -0,0 +1,335 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.33">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name=".space"/>
+ <GlyphID id="2" name="A"/>
+ <GlyphID id="3" name="B"/>
+ <GlyphID id="4" name="A.0"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0x49d0234e"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00000011"/>
+ <unitsPerEm value="1024"/>
+ <created value="Tue Jul 5 12:29:44 2022"/>
+ <modified value="Tue Jul 5 12:29:44 2022"/>
+ <xMin value="51"/>
+ <yMin value="-250"/>
+ <xMax value="878"/>
+ <yMax value="950"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="6"/>
+ <fontDirectionHint value="2"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="950"/>
+ <descent value="-250"/>
+ <lineGap value="0"/>
+ <advanceWidthMax value="1275"/>
+ <minLeftSideBearing value="51"/>
+ <minRightSideBearing value="397"/>
+ <xMaxExtent value="878"/>
+ <caretSlopeRise value="1"/>
+ <caretSlopeRun value="0"/>
+ <caretOffset value="0"/>
+ <reserved0 value="0"/>
+ <reserved1 value="0"/>
+ <reserved2 value="0"/>
+ <reserved3 value="0"/>
+ <metricDataFormat value="0"/>
+ <numberOfHMetrics value="1"/>
+ </hhea>
+
+ <maxp>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="0x10000"/>
+ <numGlyphs value="5"/>
+ <maxPoints value="16"/>
+ <maxContours value="2"/>
+ <maxCompositePoints value="0"/>
+ <maxCompositeContours value="0"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="0"/>
+ <maxComponentDepth value="0"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="1275"/>
+ <usWeightClass value="400"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000100"/>
+ <ySubscriptXSize value="666"/>
+ <ySubscriptYSize value="614"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="77"/>
+ <ySuperscriptXSize value="666"/>
+ <ySuperscriptYSize value="614"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="358"/>
+ <yStrikeoutSize value="51"/>
+ <yStrikeoutPosition value="307"/>
+ <sFamilyClass value="0"/>
+ <panose>
+ <bFamilyType value="0"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="0"/>
+ <bProportion value="0"/>
+ <bContrast value="0"/>
+ <bStrokeVariation value="0"/>
+ <bArmStyle value="0"/>
+ <bLetterForm value="0"/>
+ <bMidline value="0"/>
+ <bXHeight value="0"/>
+ </panose>
+ <ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
+ <ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
+ <achVendID value="NONE"/>
+ <fsSelection value="00000000 11000000"/>
+ <usFirstCharIndex value="32"/>
+ <usLastCharIndex value="66"/>
+ <sTypoAscender value="950"/>
+ <sTypoDescender value="-250"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="950"/>
+ <usWinDescent value="250"/>
+ <ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
+ <ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
+ <sxHeight value="512"/>
+ <sCapHeight value="717"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="0"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="1275" lsb="51"/>
+ <mtx name=".space" width="1275" lsb="0"/>
+ <mtx name="A" width="1275" lsb="0"/>
+ <mtx name="A.0" width="1275" lsb="398"/>
+ <mtx name="B" width="1275" lsb="0"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x20" name=".space"/><!-- SPACE -->
+ <map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
+ <map code="0x42" name="B"/><!-- LATIN CAPITAL LETTER B -->
+ </cmap_format_4>
+ </cmap>
+
+ <loca>
+ <!-- The 'loca' table will be calculated by the compiler -->
+ </loca>
+
+ <glyf>
+
+ <!-- The xMin, yMin, xMax and yMax values
+ will be recalculated by the compiler. -->
+
+ <TTGlyph name=".notdef" xMin="51" yMin="-250" xMax="461" yMax="950">
+ <contour>
+ <pt x="51" y="-250" on="1"/>
+ <pt x="51" y="950" on="1"/>
+ <pt x="461" y="950" on="1"/>
+ <pt x="461" y="-250" on="1"/>
+ </contour>
+ <contour>
+ <pt x="102" y="-199" on="1"/>
+ <pt x="410" y="-199" on="1"/>
+ <pt x="410" y="899" on="1"/>
+ <pt x="102" y="899" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name=".space"/><!-- contains no outline data -->
+
+ <TTGlyph name="A"/><!-- contains no outline data -->
+
+ <TTGlyph name="A.0" xMin="398" yMin="110" xMax="878" yMax="590">
+ <contour>
+ <pt x="878" y="350" on="1"/>
+ <pt x="878" y="416" on="0"/>
+ <pt x="813" y="525" on="0"/>
+ <pt x="704" y="590" on="0"/>
+ <pt x="638" y="590" on="1"/>
+ <pt x="571" y="590" on="0"/>
+ <pt x="462" y="525" on="0"/>
+ <pt x="398" y="416" on="0"/>
+ <pt x="398" y="350" on="1"/>
+ <pt x="398" y="284" on="0"/>
+ <pt x="462" y="175" on="0"/>
+ <pt x="571" y="110" on="0"/>
+ <pt x="638" y="110" on="1"/>
+ <pt x="704" y="110" on="0"/>
+ <pt x="813" y="175" on="0"/>
+ <pt x="878" y="284" on="0"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="B"/><!-- contains no outline data -->
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ 1.000;NONE;AnEmojiFamily-Regular
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ An Emoji Family Regular
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 1.000
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ AnEmojiFamily-Regular
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="2.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-77"/>
+ <underlineThickness value="51"/>
+ <isFixedPitch value="0"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="0"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="0"/>
+ <psNames>
+ <!-- This file uses unique glyph names based on the information
+ found in the 'post' table. Since these names might not be unique,
+ we have to invent artificial names in case of clashes. In order to
+ be able to retain the original information, we need a name to
+ ps name mapping for those cases where they differ. That's what
+ you see below.
+ -->
+ </psNames>
+ <extraNames>
+ <!-- following are the name that are not taken from the standard Mac glyph order -->
+ <psName name=".space"/>
+ <psName name="A.0"/>
+ </extraNames>
+ </post>
+
+ <COLR>
+ <Version value="1"/>
+ <!-- BaseGlyphRecordCount=0 -->
+ <!-- LayerRecordCount=0 -->
+ <BaseGlyphList>
+ <!-- BaseGlyphCount=2 -->
+ <BaseGlyphPaintRecord index="0">
+ <BaseGlyph value="A"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="3"/>
+ <FirstLayerIndex value="0"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ <BaseGlyphPaintRecord index="1">
+ <BaseGlyph value="B"/>
+ <Paint Format="1"><!-- PaintColrLayers -->
+ <NumLayers value="2"/>
+ <FirstLayerIndex value="1"/>
+ </Paint>
+ </BaseGlyphPaintRecord>
+ </BaseGlyphList>
+ <LayerList>
+ <!-- LayerCount=3 -->
+ <Paint index="0" Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="0"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <Paint index="1" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="2"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-120"/>
+ </Paint>
+ <Paint index="2" Format="14"><!-- PaintTranslate -->
+ <Paint Format="10"><!-- PaintGlyph -->
+ <Paint Format="2"><!-- PaintSolid -->
+ <PaletteIndex value="1"/>
+ <Alpha value="1.0"/>
+ </Paint>
+ <Glyph value="A.0"/>
+ </Paint>
+ <dx value="0"/>
+ <dy value="-240"/>
+ </Paint>
+ </LayerList>
+ <ClipList Format="1">
+ <Clip>
+ <Glyph value="A"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="600"/>
+ </ClipBox>
+ </Clip>
+ <Clip>
+ <Glyph value="B"/>
+ <ClipBox Format="1">
+ <xMin value="380"/>
+ <yMin value="-140"/>
+ <xMax value="880"/>
+ <yMax value="480"/>
+ </ClipBox>
+ </Clip>
+ </ClipList>
+ </COLR>
+
+ <CPAL>
+ <version value="0"/>
+ <numPaletteEntries value="3"/>
+ <palette index="0">
+ <color index="0" value="#0000FFFF"/>
+ <color index="1" value="#008000FF"/>
+ <color index="2" value="#FF0000FF"/>
+ </palette>
+ </CPAL>
+
+</ttFont>
diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
index 14e12097..0086ddc3 100644
--- a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_diff.ttx
+++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_diff.ttx
@@ -83,21 +83,23 @@
</MarkBasePos>
</Lookup>
<Lookup index="2">
- <LookupType value="7"/>
+ <LookupType value="8"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ContextPos index="0" Format="1">
+ <ChainContextPos index="0" Format="1">
<Coverage>
<Glyph value="A"/>
</Coverage>
- <!-- PosRuleSetCount=1 -->
- <PosRuleSet index="0">
- <!-- PosRuleCount=1 -->
- <PosRule index="0">
- <!-- GlyphCount=3 -->
- <!-- PosCount=2 -->
+ <!-- ChainPosRuleSetCount=1 -->
+ <ChainPosRuleSet index="0">
+ <!-- ChainPosRuleCount=1 -->
+ <ChainPosRule index="0">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=3 -->
<Input index="0" value="a"/>
<Input index="1" value="uni0303"/>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- PosCount=2 -->
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
@@ -106,9 +108,9 @@
<SequenceIndex value="2"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </PosRule>
- </PosRuleSet>
- </ContextPos>
+ </ChainPosRule>
+ </ChainPosRuleSet>
+ </ChainContextPos>
</Lookup>
</LookupList>
</GPOS>
diff --git a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
index eff24fc3..a32c94d0 100644
--- a/Tests/varLib/data/test_results/InterpolateLayoutGPOS_7_same.ttx
+++ b/Tests/varLib/data/test_results/InterpolateLayoutGPOS_8_same.ttx
@@ -83,21 +83,23 @@
</MarkBasePos>
</Lookup>
<Lookup index="2">
- <LookupType value="7"/>
+ <LookupType value="8"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
- <ContextPos index="0" Format="1">
+ <ChainContextPos index="0" Format="1">
<Coverage>
<Glyph value="A"/>
</Coverage>
- <!-- PosRuleSetCount=1 -->
- <PosRuleSet index="0">
- <!-- PosRuleCount=1 -->
- <PosRule index="0">
- <!-- GlyphCount=3 -->
- <!-- PosCount=2 -->
+ <!-- ChainPosRuleSetCount=1 -->
+ <ChainPosRuleSet index="0">
+ <!-- ChainPosRuleCount=1 -->
+ <ChainPosRule index="0">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=3 -->
<Input index="0" value="a"/>
<Input index="1" value="uni0303"/>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- PosCount=2 -->
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
@@ -106,9 +108,9 @@
<SequenceIndex value="2"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
- </PosRule>
- </PosRuleSet>
- </ContextPos>
+ </ChainPosRule>
+ </ChainPosRuleSet>
+ </ChainContextPos>
</Lookup>
</LookupList>
</GPOS>
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 bb2d1758..db224cca 100644
--- a/Tests/varLib/instancer/instancer_test.py
+++ b/Tests/varLib/instancer/instancer_test.py
@@ -1,4 +1,5 @@
from fontTools.misc.fixedTools import floatToFixedToFloat
+from fontTools.misc.testTools import stripVariableItemsFromTTX
from fontTools.misc.textTools import Tag
from fontTools import ttLib
from fontTools import designspaceLib
@@ -457,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([])
@@ -1386,10 +1389,6 @@ def test_setMacOverlapFlags():
assert b.components[0].flags & flagOverlapCompound != 0
-def _strip_ttLibVersion(string):
- return re.sub(' ttLibVersion=".*"', "", string)
-
-
@pytest.fixture
def varfont2():
f = ttLib.TTFont(recalcTimestamp=False)
@@ -1412,7 +1411,7 @@ def _dump_ttx(ttFont):
ttFont2 = ttLib.TTFont(tmp, recalcBBoxes=False, recalcTimestamp=False)
s = StringIO()
ttFont2.saveXML(s)
- return _strip_ttLibVersion(s.getvalue())
+ return stripVariableItemsFromTTX(s.getvalue())
def _get_expected_instance_ttx(
@@ -1428,7 +1427,7 @@ def _get_expected_instance_ttx(
"r",
encoding="utf-8",
) as fp:
- return _strip_ttLibVersion(fp.read())
+ return stripVariableItemsFromTTX(fp.read())
class InstantiateVariableFontTest(object):
@@ -1978,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/interpolate_layout_test.py b/Tests/varLib/interpolate_layout_test.py
index 18dc3a75..219f087f 100644
--- a/Tests/varLib/interpolate_layout_test.py
+++ b/Tests/varLib/interpolate_layout_test.py
@@ -746,8 +746,8 @@ class InterpolateLayoutTest(unittest.TestCase):
self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
- def test_varlib_interpolate_layout_GPOS_only_LookupType_7_same_val_ttf(self):
- """Only GPOS; LookupType 7; same values in all masters.
+ def test_varlib_interpolate_layout_GPOS_only_LookupType_8_same_val_ttf(self):
+ """Only GPOS; LookupType 8; same values in all masters.
"""
suffix = '.ttf'
ds_path = self.get_test_input('InterpolateLayout.designspace')
@@ -779,13 +779,13 @@ class InterpolateLayoutTest(unittest.TestCase):
instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
tables = ['GPOS']
- expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_7_same.ttx')
+ expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_same.ttx')
self.expect_ttx(instfont, expected_ttx_path, tables)
self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
- def test_varlib_interpolate_layout_GPOS_only_LookupType_7_diff_val_ttf(self):
- """Only GPOS; LookupType 7; different values in each master.
+ def test_varlib_interpolate_layout_GPOS_only_LookupType_8_diff_val_ttf(self):
+ """Only GPOS; LookupType 8; different values in each master.
"""
suffix = '.ttf'
ds_path = self.get_test_input('InterpolateLayout.designspace')
@@ -831,7 +831,7 @@ class InterpolateLayoutTest(unittest.TestCase):
instfont = interpolate_layout(ds_path, {'weight': 500}, finder)
tables = ['GPOS']
- expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_7_diff.ttx')
+ expected_ttx_path = self.get_test_output('InterpolateLayoutGPOS_8_diff.ttx')
self.expect_ttx(instfont, expected_ttx_path, tables)
self.check_ttx_dump(instfont, expected_ttx_path, tables, suffix)
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
new file mode 100644
index 00000000..6def990e
--- /dev/null
+++ b/Tests/varLib/stat_test.py
@@ -0,0 +1,167 @@
+from pathlib import Path
+
+import pytest
+from fontTools.designspaceLib import DesignSpaceDocument
+from fontTools.designspaceLib.split import Range
+from fontTools.varLib.stat import getStatAxes, getStatLocations
+
+
+@pytest.fixture
+def datadir():
+ return Path(__file__).parent / "../designspaceLib/data"
+
+
+def test_getStatAxes(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
+
+ assert getStatAxes(
+ doc, {"Italic": 0, "Width": Range(50, 150), "Weight": Range(200, 900)}
+ ) == [
+ {
+ "values": [
+ {
+ "flags": 0,
+ "name": {
+ "de": "Extraleicht",
+ "en": "Extra Light",
+ "fr": "Extra léger",
+ },
+ "nominalValue": 200.0,
+ "rangeMaxValue": 250.0,
+ "rangeMinValue": 200.0,
+ },
+ {
+ "flags": 0,
+ "name": {"en": "Light"},
+ "nominalValue": 300.0,
+ "rangeMaxValue": 350.0,
+ "rangeMinValue": 250.0,
+ },
+ {
+ "flags": 2,
+ "name": {"en": "Regular"},
+ "nominalValue": 400.0,
+ "rangeMaxValue": 450.0,
+ "rangeMinValue": 350.0,
+ },
+ {
+ "flags": 0,
+ "name": {"en": "Semi Bold"},
+ "nominalValue": 600.0,
+ "rangeMaxValue": 650.0,
+ "rangeMinValue": 450.0,
+ },
+ {
+ "flags": 0,
+ "name": {"en": "Bold"},
+ "nominalValue": 700.0,
+ "rangeMaxValue": 850.0,
+ "rangeMinValue": 650.0,
+ },
+ {
+ "flags": 0,
+ "name": {"en": "Black"},
+ "nominalValue": 900.0,
+ "rangeMaxValue": 900.0,
+ "rangeMinValue": 850.0,
+ },
+ ],
+ "name": {"en": "Wéíght", "fa-IR": "قطر"},
+ "ordering": 2,
+ "tag": "wght",
+ },
+ {
+ "values": [
+ {"flags": 0, "name": {"en": "Condensed"}, "value": 50.0},
+ {"flags": 3, "name": {"en": "Normal"}, "value": 100.0},
+ {"flags": 0, "name": {"en": "Wide"}, "value": 125.0},
+ {
+ "flags": 0,
+ "name": {"en": "Extra Wide"},
+ "nominalValue": 150.0,
+ "rangeMinValue": 150.0,
+ },
+ ],
+ "name": {"en": "Width", "fr": "Chasse"},
+ "ordering": 1,
+ "tag": "wdth",
+ },
+ {
+ "values": [
+ {"flags": 2, "linkedValue": 1.0, "name": {"en": "Roman"}, "value": 0.0},
+ ],
+ "name": {"en": "Italic"},
+ "ordering": 3,
+ "tag": "ital",
+ },
+ ]
+
+ assert getStatAxes(doc, {"Italic": 1, "Width": 100, "Weight": Range(400, 700)}) == [
+ {
+ "values": [
+ {
+ "flags": 2,
+ "name": {"en": "Regular"},
+ "nominalValue": 400.0,
+ "rangeMaxValue": 450.0,
+ "rangeMinValue": 350.0,
+ },
+ {
+ "flags": 0,
+ "name": {"en": "Semi Bold"},
+ "nominalValue": 600.0,
+ "rangeMaxValue": 650.0,
+ "rangeMinValue": 450.0,
+ },
+ {
+ "flags": 0,
+ "name": {"en": "Bold"},
+ "nominalValue": 700.0,
+ "rangeMaxValue": 850.0,
+ "rangeMinValue": 650.0,
+ },
+ ],
+ "name": {"en": "Wéíght", "fa-IR": "قطر"},
+ "ordering": 2,
+ "tag": "wght",
+ },
+ {
+ "values": [
+ {"flags": 3, "name": {"en": "Normal"}, "value": 100.0},
+ ],
+ "name": {"en": "Width", "fr": "Chasse"},
+ "ordering": 1,
+ "tag": "wdth",
+ },
+ {
+ "values": [
+ {"flags": 0, "name": {"en": "Italic"}, "value": 1.0},
+ ],
+ "name": {"en": "Italic"},
+ "ordering": 3,
+ "tag": "ital",
+ },
+ ]
+
+
+def test_getStatLocations(datadir):
+ doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace")
+
+ assert getStatLocations(
+ doc, {"Italic": 0, "Width": Range(50, 150), "Weight": Range(200, 900)}
+ ) == [
+ {
+ "flags": 0,
+ "location": {"ital": 0.0, "wdth": 50.0, "wght": 300.0},
+ "name": {"en": "Some Style", "fr": "Un Style"},
+ },
+ ]
+ assert getStatLocations(
+ doc, {"Italic": 1, "Width": Range(50, 150), "Weight": Range(200, 900)}
+ ) == [
+ {
+ "flags": 0,
+ "location": {"ital": 1.0, "wdth": 100.0, "wght": 700.0},
+ "name": {"en": "Other"},
+ },
+ ]
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 d061b779..1bab6d77 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,12 +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
+ufoLib2==0.13.1
+pyobjc==8.5; sys_platform == "darwin"
+freetype-py==2.3.0
+uharfbuzz==0.30.0
diff --git a/setup.cfg b/setup.cfg
index 7c7fc00c..69bfde14 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.31.2
+current_version = 4.37.1
commit = True
tag = False
tag_name = {new_version}
@@ -52,6 +52,8 @@ filterwarnings =
ignore:writePlist:DeprecationWarning:plistlib_test
ignore:some_function:DeprecationWarning:fontTools.ufoLib.utils
ignore::DeprecationWarning:fontTools.varLib.designspace
+markers =
+ slow: marks tests as slow (deselect with '-m "not slow"')
[tool:interrogate]
ignore-semiprivate = true
diff --git a/setup.py b/setup.py
index 2f30ef4f..03395ac5 100755
--- a/setup.py
+++ b/setup.py
@@ -68,6 +68,12 @@ if with_cython is True or (with_cython is None and has_cython):
ext_modules.append(
Extension("fontTools.cu2qu.cu2qu", ["Lib/fontTools/cu2qu/cu2qu.py"]),
)
+ ext_modules.append(
+ Extension("fontTools.pens.momentsPen", ["Lib/fontTools/pens/momentsPen.py"]),
+ )
+ ext_modules.append(
+ Extension("fontTools.varLib.iup", ["Lib/fontTools/varLib/iup.py"]),
+ )
extras_require = {
# for fontTools.ufoLib: to read/write UFO fonts
@@ -124,6 +130,10 @@ extras_require = {
"pathops": [
"skia-pathops >= 0.5.0",
],
+ # for packing GSUB/GPOS tables with Harfbuzz repacker
+ "repacker": [
+ "uharfbuzz >= 0.23.0",
+ ],
}
# use a special 'all' key as shorthand to includes all the extra dependencies
extras_require["all"] = sum(extras_require.values(), [])
@@ -439,7 +449,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.31.2",
+ version="4.37.1",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",