aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-x.travis/run.sh4
-rw-r--r--Doc/docs-requirements.txt2
-rw-r--r--Doc/source/afmLib.rst10
-rw-r--r--Doc/source/agl.rst10
-rw-r--r--Doc/source/cffLib/index.rst49
-rw-r--r--Doc/source/cffLib/specializer.rst6
-rw-r--r--Doc/source/cffLib/width.rst10
-rw-r--r--Doc/source/colorLib/builder.rst8
-rw-r--r--Doc/source/colorLib/errors.rst8
-rw-r--r--Doc/source/colorLib/index.rst17
-rw-r--r--Doc/source/conf.py2
-rw-r--r--Doc/source/cu2qu/cli.rst8
-rw-r--r--Doc/source/cu2qu/cu2qu.rst8
-rw-r--r--Doc/source/cu2qu/errors.rst8
-rw-r--r--Doc/source/cu2qu/index.rst44
-rw-r--r--Doc/source/cu2qu/ufo.rst8
-rw-r--r--Doc/source/developer.rst115
-rw-r--r--Doc/source/encodings/StandardEncoding.rst10
-rw-r--r--Doc/source/encodings/codecs.rst8
-rw-r--r--Doc/source/encodings/index.rst30
-rw-r--r--Doc/source/encodings/macRoman.rst10
-rw-r--r--Doc/source/feaLib/ast.rst8
-rw-r--r--Doc/source/feaLib/builder.rst8
-rw-r--r--Doc/source/feaLib/error.rst8
-rw-r--r--Doc/source/feaLib/index.rst49
-rw-r--r--Doc/source/feaLib/lexer.rst8
-rw-r--r--Doc/source/feaLib/parser.rst8
-rw-r--r--Doc/source/index.rst375
-rw-r--r--Doc/source/merge.rst12
-rw-r--r--Doc/source/optional.rst140
-rw-r--r--Doc/source/ttx.rst54
-rw-r--r--LICENSE.external4
-rw-r--r--Lib/fontTools/__init__.py2
-rw-r--r--Lib/fontTools/__main__.py6
-rw-r--r--Lib/fontTools/afmLib.py73
-rw-r--r--Lib/fontTools/agl.py26
-rw-r--r--Lib/fontTools/cffLib/__init__.py236
-rw-r--r--Lib/fontTools/cffLib/specializer.py13
-rw-r--r--Lib/fontTools/cffLib/width.py40
-rw-r--r--Lib/fontTools/colorLib/builder.py4
-rw-r--r--Lib/fontTools/cu2qu/cli.py1
-rw-r--r--Lib/fontTools/cu2qu/cu2qu.py174
-rw-r--r--Lib/fontTools/cu2qu/errors.py2
-rw-r--r--Lib/fontTools/feaLib/__main__.py1
-rw-r--r--Lib/fontTools/feaLib/ast.py387
-rw-r--r--Lib/fontTools/feaLib/builder.py116
-rw-r--r--Lib/fontTools/feaLib/parser.py84
-rw-r--r--Lib/fontTools/fontBuilder.py9
-rw-r--r--Lib/fontTools/help.py34
-rw-r--r--Lib/fontTools/merge.py55
-rw-r--r--Lib/fontTools/misc/loggingTools.py4
-rw-r--r--Lib/fontTools/misc/psCharStrings.py5
-rw-r--r--Lib/fontTools/misc/testTools.py3
-rw-r--r--Lib/fontTools/mtiLib/__init__.py29
-rw-r--r--Lib/fontTools/otlLib/builder.py191
-rw-r--r--Lib/fontTools/subset/__init__.py8
-rw-r--r--Lib/fontTools/subset/__main__.py1
-rw-r--r--Lib/fontTools/ttLib/sfnt.py7
-rw-r--r--Lib/fontTools/ttLib/tables/E_B_L_C_.py2
-rw-r--r--Lib/fontTools/ttLib/tables/_g_v_a_r.py2
-rw-r--r--Lib/fontTools/ttLib/tables/_m_e_t_a.py6
-rw-r--r--Lib/fontTools/ttLib/tables/_n_a_m_e.py58
-rw-r--r--Lib/fontTools/ttLib/woff2.py47
-rw-r--r--Lib/fontTools/ttx.py1
-rwxr-xr-xLib/fontTools/ufoLib/__init__.py269
-rw-r--r--Lib/fontTools/ufoLib/errors.py8
-rwxr-xr-xLib/fontTools/ufoLib/glifLib.py256
-rw-r--r--Lib/fontTools/ufoLib/utils.py33
-rw-r--r--Lib/fontTools/unicodedata/__init__.py10
-rw-r--r--Lib/fontTools/varLib/__init__.py29
-rw-r--r--Lib/fontTools/varLib/__main__.py1
-rw-r--r--Lib/fontTools/varLib/cff.py6
-rw-r--r--Lib/fontTools/varLib/featureVars.py5
-rw-r--r--Lib/fontTools/varLib/instancer.py3
-rw-r--r--Lib/fontTools/varLib/interpolatable.py19
-rw-r--r--Lib/fontTools/varLib/interpolate_layout.py37
-rw-r--r--Lib/fontTools/varLib/merger.py6
-rw-r--r--Lib/fontTools/varLib/models.py32
-rw-r--r--Lib/fontTools/varLib/mutator.py1
-rw-r--r--Lib/fontTools/varLib/varStore.py3
-rw-r--r--Lib/fonttools.egg-info/PKG-INFO209
-rw-r--r--Lib/fonttools.egg-info/SOURCES.txt26
-rw-r--r--METADATA8
-rwxr-xr-xMetaTools/buildTableList.py7
-rwxr-xr-xMetaTools/roundTrip.py6
-rw-r--r--NEWS.rst28
-rw-r--r--PKG-INFO209
-rw-r--r--README.rst167
-rw-r--r--Tests/designspaceLib/designspace_test.py25
-rw-r--r--Tests/feaLib/builder_test.py3
-rw-r--r--Tests/feaLib/data/MultipleLookupsPerGlyph.fea11
-rw-r--r--Tests/feaLib/data/MultipleLookupsPerGlyph.ttx76
-rw-r--r--Tests/feaLib/data/MultipleLookupsPerGlyph2.fea11
-rw-r--r--Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx84
-rw-r--r--Tests/feaLib/parser_test.py6
-rw-r--r--Tests/fontBuilder/data/test_var.ttf.ttx86
-rw-r--r--Tests/fontBuilder/fontBuilder_test.py7
-rw-r--r--Tests/merge_test.py52
-rw-r--r--Tests/otlLib/builder_test.py289
-rw-r--r--Tests/subset/data/TestContextSubstFormat3.ttx610
-rw-r--r--Tests/subset/subset_test.py13
-rw-r--r--Tests/ttLib/tables/_g_v_a_r_test.py18
-rw-r--r--Tests/ttLib/tables/_m_e_t_a_test.py13
-rw-r--r--Tests/ttLib/tables/_n_a_m_e_test.py42
-rw-r--r--Tests/ufoLib/glifLib_test.py119
-rw-r--r--Tests/ufoLib/ufoLib_test.py98
-rw-r--r--Tests/varLib/data/FeatureVarsWholeRange.designspace34
-rw-r--r--Tests/varLib/data/FeatureVarsWholeRangeEmpty.designspace33
-rw-r--r--Tests/varLib/data/test_results/Build.ttx1006
-rw-r--r--Tests/varLib/data/test_results/BuildMain.ttx1006
-rw-r--r--Tests/varLib/data/test_results/FeatureVarsWholeRange.ttx75
-rw-r--r--Tests/varLib/data/test_results/SparseMasters.ttx38
-rw-r--r--Tests/varLib/varLib_test.py24
-rw-r--r--setup.cfg3
-rwxr-xr-xsetup.py2
-rw-r--r--tox.ini3
116 files changed, 5283 insertions, 2558 deletions
diff --git a/.travis/run.sh b/.travis/run.sh
index ffb0ef79..e947d850 100755
--- a/.travis/run.sh
+++ b/.travis/run.sh
@@ -7,7 +7,7 @@ if [ "$TRAVIS_OS_NAME" == "osx" ]; then
source .venv/bin/activate
fi
-tox
+tox --skip-missing-interpreters false
# re-run all the XML-related tests, this time without lxml but using the
# built-in ElementTree library.
@@ -17,4 +17,4 @@ else
# strip additional tox envs after the comma, add -nolxml factor
TOXENV="${TOXENV%,*}-nolxml"
fi
-tox -e $TOXENV -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py
+tox --skip-missing-interpreters false -e $TOXENV -- Tests/ufoLib Tests/misc/etree_test.py Tests/misc/plistlib_test.py
diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt
index 9170d92a..998172d5 100644
--- a/Doc/docs-requirements.txt
+++ b/Doc/docs-requirements.txt
@@ -1,3 +1,3 @@
-sphinx == 3.0.2
+sphinx==3.0.3
sphinx_rtd_theme == 0.4.3
reportlab == 3.5.42
diff --git a/Doc/source/afmLib.rst b/Doc/source/afmLib.rst
index b9c419fe..ab9f3567 100644
--- a/Doc/source/afmLib.rst
+++ b/Doc/source/afmLib.rst
@@ -1,8 +1,8 @@
-######
-afmLib
-######
+###########################################
+afmLib: Read/write Adobe Font Metrics files
+###########################################
.. automodule:: fontTools.afmLib
- :inherited-members:
+
+.. autoclass:: fontTools.afmLib.AFM
:members:
- :undoc-members:
diff --git a/Doc/source/agl.rst b/Doc/source/agl.rst
index 84fb56fa..6e89857f 100644
--- a/Doc/source/agl.rst
+++ b/Doc/source/agl.rst
@@ -1,8 +1,6 @@
-###
-agl
-###
+######################################
+agl: Interface to the Adobe Glyph List
+######################################
.. automodule:: fontTools.agl
- :inherited-members:
- :members:
- :undoc-members:
+ :members: toUnicode, UV2AGL, AGL2UV
diff --git a/Doc/source/cffLib/index.rst b/Doc/source/cffLib/index.rst
index be8073e3..281a0b12 100644
--- a/Doc/source/cffLib/index.rst
+++ b/Doc/source/cffLib/index.rst
@@ -1,6 +1,10 @@
-######
-cffLib
-######
+##################################
+cffLib: read/write Adobe CFF fonts
+##################################
+
+.. automodule:: fontTools.cffLib
+
+This package also contains two modules for manipulating CFF format glyphs:
.. toctree::
:maxdepth: 1
@@ -8,7 +12,42 @@ cffLib
specializer
width
-.. automodule:: fontTools.cffLib
+.. autoclass:: fontTools.cffLib.CFFFontSet
:inherited-members:
:members:
- :undoc-members:
+
+.. autoclass:: fontTools.cffLib.TopDict
+ :members:
+
+.. autoclass:: fontTools.cffLib.CharStrings
+ :members:
+
+.. autoclass:: fontTools.cffLib.Index
+ :members:
+
+.. autoclass:: fontTools.cffLib.GlobalSubrsIndex
+ :members:
+
+.. autoclass:: fontTools.cffLib.TopDictIndex
+ :members:
+
+.. autoclass:: fontTools.cffLib.CFFWriter
+ :members:
+
+.. autoclass:: fontTools.cffLib.IndexCompiler
+ :members:
+
+.. autoclass:: fontTools.cffLib.TopDictIndexCompiler
+ :members:
+
+.. autoclass:: fontTools.cffLib.FDArrayIndexCompiler
+ :members:
+
+.. autoclass:: fontTools.cffLib.GlobalSubrsCompiler
+ :members:
+
+.. autoclass:: fontTools.cffLib.SubrsCompiler
+ :members:
+
+.. autoclass:: fontTools.cffLib.CharStringsCompiler
+ :members:
diff --git a/Doc/source/cffLib/specializer.rst b/Doc/source/cffLib/specializer.rst
index 1d6645c7..016a8962 100644
--- a/Doc/source/cffLib/specializer.rst
+++ b/Doc/source/cffLib/specializer.rst
@@ -1,6 +1,6 @@
-###########
-specializer
-###########
+##############################################################
+specializer: T2CharString operator specializer and generalizer
+##############################################################
.. automodule:: fontTools.cffLib.specializer
:inherited-members:
diff --git a/Doc/source/cffLib/width.rst b/Doc/source/cffLib/width.rst
index 704a9aa4..68944da8 100644
--- a/Doc/source/cffLib/width.rst
+++ b/Doc/source/cffLib/width.rst
@@ -1,8 +1,6 @@
-#####
-width
-#####
+#########################################
+width: T2CharString glyph width optimizer
+#########################################
.. automodule:: fontTools.cffLib.width
- :inherited-members:
- :members:
- :undoc-members:
+ :members: optimizeWidths, optimizeWidthsBruteforce
diff --git a/Doc/source/colorLib/builder.rst b/Doc/source/colorLib/builder.rst
deleted file mode 100644
index 94170f1d..00000000
--- a/Doc/source/colorLib/builder.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-#######
-builder
-#######
-
-.. automodule:: fontTools.colorLib.builder
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/colorLib/errors.rst b/Doc/source/colorLib/errors.rst
deleted file mode 100644
index c06224b2..00000000
--- a/Doc/source/colorLib/errors.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-######
-errors
-######
-
-.. automodule:: fontTools.colorLib.errors
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/colorLib/index.rst b/Doc/source/colorLib/index.rst
index 06721bea..d4eb9f83 100644
--- a/Doc/source/colorLib/index.rst
+++ b/Doc/source/colorLib/index.rst
@@ -1,14 +1,11 @@
-########
-colorLib
-########
+#####################################################
+colorLib.builder: Build COLR/CPAL tables from scratch
+#####################################################
-.. toctree::
- :maxdepth: 1
+.. automodule:: fontTools.colorLib.builder
+ :members: buildCPAL, buildCOLR, populateCOLRv0
- builder
- errors
-
-.. automodule:: fontTools.colorLib
+.. autoclass:: fontTools.colorLib.builder.ColorPaletteType
:inherited-members:
:members:
- :undoc-members: \ No newline at end of file
+ :undoc-members:
diff --git a/Doc/source/conf.py b/Doc/source/conf.py
index e0ee7a93..82a5d579 100644
--- a/Doc/source/conf.py
+++ b/Doc/source/conf.py
@@ -30,7 +30,7 @@ needs_sphinx = "1.3"
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon"]
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon", "sphinx.ext.coverage"]
autodoc_mock_imports = ["gtk"]
diff --git a/Doc/source/cu2qu/cli.rst b/Doc/source/cu2qu/cli.rst
deleted file mode 100644
index 798b49f2..00000000
--- a/Doc/source/cu2qu/cli.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-###
-cli
-###
-
-.. automodule:: fontTools.cu2qu.cli
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/cu2qu/cu2qu.rst b/Doc/source/cu2qu/cu2qu.rst
deleted file mode 100644
index 73f95c96..00000000
--- a/Doc/source/cu2qu/cu2qu.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-#####
-cu2qu
-#####
-
-.. automodule:: fontTools.cu2qu.cu2qu
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/cu2qu/errors.rst b/Doc/source/cu2qu/errors.rst
deleted file mode 100644
index 679c7514..00000000
--- a/Doc/source/cu2qu/errors.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-######
-errors
-######
-
-.. automodule:: fontTools.cu2qu.errors
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/cu2qu/index.rst b/Doc/source/cu2qu/index.rst
index d8c00310..41730e54 100644
--- a/Doc/source/cu2qu/index.rst
+++ b/Doc/source/cu2qu/index.rst
@@ -1,16 +1,38 @@
-#####
-cu2qu
-#####
+##########################################
+cu2qu: Cubic to quadratic curve conversion
+##########################################
-.. toctree::
- :maxdepth: 1
+Routines for converting cubic curves to quadratic splines, suitable for use
+in OpenType to TrueType outline conversion.
- cli
- cu2qu
- errors
- ufo
+Conversion is carried out to a degree of tolerance provided by the user. While
+it is relatively easy to find the best *single* quadratic curve to represent a
+given cubic (see for example `this method from CAGD <https://www.sirver.net/blog/2011/08/23/degree-reduction-of-bezier-curves/>`_),
+the best-fit method may not be sufficiently accurate for type design.
-.. automodule:: fontTools.cu2qu
+Instead, this method chops the cubic curve into multiple segments before
+converting each cubic segment to a quadratic, in order to ensure that the
+resulting spline fits within the given tolerance.
+
+The basic curve conversion routines are implemented in the
+:mod:`fontTools.cu2qu.cu2qu` module; the :mod:`fontTools.cu2qu.ufo` module
+applies these routines to all of the curves in a UFO file or files; while the
+:mod:`fontTools.cu2qu.cli` module implements the ``fonttools cu2qu`` command
+for converting a UFO format font with cubic curves into one with quadratic
+curves.
+
+fontTools.cu2qu.cu2qu
+---------------------
+
+.. automodule:: fontTools.cu2qu.cu2qu
+ :inherited-members:
+ :members:
+ :undoc-members:
+
+fontTools.cu2qu.ufo
+-------------------
+
+.. automodule:: fontTools.cu2qu.ufo
:inherited-members:
:members:
- :undoc-members: \ No newline at end of file
+ :undoc-members:
diff --git a/Doc/source/cu2qu/ufo.rst b/Doc/source/cu2qu/ufo.rst
deleted file mode 100644
index ef66e74b..00000000
--- a/Doc/source/cu2qu/ufo.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-###
-ufo
-###
-
-.. automodule:: fontTools.cu2qu.ufo
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/developer.rst b/Doc/source/developer.rst
new file mode 100644
index 00000000..3e259f0e
--- /dev/null
+++ b/Doc/source/developer.rst
@@ -0,0 +1,115 @@
+.. _developerinfo:
+.. image:: ../../Icons/FontToolsIconGreenCircle.png
+ :width: 200px
+ :height: 200px
+ :alt: Font Tools
+ :align: center
+
+
+fontTools Developer Information
+===============================
+
+If you would like to contribute to the development of fontTools, you can clone the repository from GitHub, install the package in 'editable' mode and modify the source code in place. We recommend creating a virtual environment, using the Python 3 `venv <https://docs.python.org/3/library/venv.html>`_ module::
+
+ # download the source code to 'fonttools' folder
+ git clone https://github.com/fonttools/fonttools.git
+ cd fonttools
+
+ # create new virtual environment called e.g. 'fonttools-venv', or anything you like
+ python -m venv fonttools-venv
+
+ # source the `activate` shell script to enter the environment (Un*x)
+ . fonttools-venv/bin/activate
+
+ # to activate the virtual environment in Windows `cmd.exe`, do
+ fonttools-venv\Scripts\activate.bat
+
+ # install in 'editable' mode
+ pip install -e .
+
+
+.. note::
+
+ To exit a Python virtual environment, enter the command ``deactivate``.
+
+Testing
+-------
+
+To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
+When you run the ``pytest`` command, the tests will run against the
+installed fontTools package, or the first one found in the
+``PYTHONPATH``.
+
+You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
+automatically run tests on different Python versions in isolated virtual
+environments::
+
+ pip install tox
+ tox
+
+
+.. note::
+
+ When you run ``tox`` without arguments, the tests are executed for all the environments listed in the ``tox.ini`` ``envlist``. The Python versions that are not available on your system ``PATH`` will be skipped.
+
+You can specify a particular testing environment list via the ``-e`` option, or the ``TOXENV`` environment variable::
+
+ tox -e py36
+ TOXENV="py36-cov,htmlcov" tox
+
+
+Development Community
+---------------------
+
+fontTools development is ongoing in an active community of developers that includes professional developers employed at major software corporations and type foundries as well as hobbyists.
+
+Feature requests and bug reports are always welcome at https://github.com/fonttools/fonttools/issues/
+
+The best place for end-user and developer discussion about the fontTools project is the `fontTools gitter channel <https://gitter.im/fonttools-dev/Lobby>`_. There is also a development https://groups.google.com/d/forum/fonttools-dev mailing list for continuous integration notifications.
+
+
+History
+-------
+
+The fontTools project was started by Just van Rossum in 1999, and was
+maintained as an open source project at
+http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
+began helping Just with stability maintenance. In 2013 Behdad Esfahbod
+began a friendly fork, thoroughly reviewing the codebase and making
+changes at https://github.com/behdad/fonttools to add new features and
+support for new font formats.
+
+
+Acknowledgments
+---------------
+
+In alphabetical order:
+
+Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland,
+Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent
+Connare, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod,
+Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis
+Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson,
+Denis Moyogo Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek,
+Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo,
+Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret
+Rieger, Read Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel,
+Georg Seifert, Chris Simpkins, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov,
+Paul Wise.
+
+License
+-------
+
+`MIT license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. See the full text of the license for details.
+
+.. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg
+ :target: https://travis-ci.org/fonttools/fonttools
+.. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true
+ :target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master
+.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/fonttools/fonttools
+.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg
+ :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
diff --git a/Doc/source/encodings/StandardEncoding.rst b/Doc/source/encodings/StandardEncoding.rst
deleted file mode 100644
index 4c936c05..00000000
--- a/Doc/source/encodings/StandardEncoding.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-################
-StandardEncoding
-################
-
-.. automodule:: fontTools.encodings.StandardEncoding
- :inherited-members:
- :members:
- :undoc-members:
-
-.. data:: fontTools.encodings.StandardEncoding.StandardEncoding
diff --git a/Doc/source/encodings/codecs.rst b/Doc/source/encodings/codecs.rst
deleted file mode 100644
index ea0b03e9..00000000
--- a/Doc/source/encodings/codecs.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-######
-codecs
-######
-
-.. automodule:: fontTools.encodings.codecs
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/encodings/index.rst b/Doc/source/encodings/index.rst
index 6de20f4f..32d13c70 100644
--- a/Doc/source/encodings/index.rst
+++ b/Doc/source/encodings/index.rst
@@ -1,17 +1,21 @@
-#########
-encodings
-#########
+##################################################
+encodings: Support for OpenType-specific encodings
+##################################################
-.. toctree::
- :maxdepth: 1
+fontTools includes support for some character encodings found in legacy Mac
+TrueType fonts. Many of these legacy encodings have found their way into the
+standard Python ``encodings`` library, but others still remain unimplemented.
+Importing ``fontTools.encodings.codecs`` will therefore add string ``encode``
+and ``decode`` support for the following encodings:
- codecs
- macRoman
- StandardEncoding
+* ``x_mac_japanese_ttx``
+* ``x_mac_trad_chinese_ttx``
+* ``x_mac_korean_ttx``
+* ``x_mac_simp_chinese_ttx``
+fontTools also includes a package (``fontTools.encodings.MacRoman``) which
+contains a mapping of glyph IDs to glyph names in the MacRoman character set::
-.. automodule:: fontTools.encodings
- :inherited-members:
- :members:
- :undoc-members:
-
+ >>> from fontTools.encodings.MacRoman import MacRoman
+ >>> MacRoman[26]
+ 'twosuperior'
diff --git a/Doc/source/encodings/macRoman.rst b/Doc/source/encodings/macRoman.rst
deleted file mode 100644
index b56d5e75..00000000
--- a/Doc/source/encodings/macRoman.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-########
-MacRoman
-########
-
-.. automodule:: fontTools.encodings.MacRoman
- :inherited-members:
- :members:
- :undoc-members:
-
-.. data:: fontTools.encodings.MacRoman.MacRoman
diff --git a/Doc/source/feaLib/ast.rst b/Doc/source/feaLib/ast.rst
deleted file mode 100644
index 6804299e..00000000
--- a/Doc/source/feaLib/ast.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-###
-ast
-###
-
-.. automodule:: fontTools.feaLib.ast
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/feaLib/builder.rst b/Doc/source/feaLib/builder.rst
deleted file mode 100644
index 8acbeada..00000000
--- a/Doc/source/feaLib/builder.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-#######
-builder
-#######
-
-.. automodule:: fontTools.feaLib.builder
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/feaLib/error.rst b/Doc/source/feaLib/error.rst
deleted file mode 100644
index fa85e0f5..00000000
--- a/Doc/source/feaLib/error.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-#####
-error
-#####
-
-.. automodule:: fontTools.feaLib.error
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/feaLib/index.rst b/Doc/source/feaLib/index.rst
index 363761db..61ac31f3 100644
--- a/Doc/source/feaLib/index.rst
+++ b/Doc/source/feaLib/index.rst
@@ -1,17 +1,40 @@
-######
-feaLib
-######
+#########################################
+feaLib: Read/write OpenType feature files
+#########################################
-.. toctree::
- :maxdepth: 1
+fontTools' ``feaLib`` allows for the creation and parsing of Adobe
+Font Development Kit for OpenType feature (``.fea``) files. The syntax
+of these files is described `here <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html>`_.
- ast
- builder
- error
- lexer
- parser
+The :class:`fontTools.feaLib.parser.Parser` class can be used to parse files
+into an abstract syntax tree, and from there the
+:class:`fontTools.feaLib.builder.Builder` class can add features to an existing
+font file. You can inspect the parsed syntax tree, walk the tree and do clever
+things with it, and also generate your own feature files programmatically, by
+using the classes in the :mod:`fontTools.feaLib.ast` module.
-.. automodule:: fontTools.feaLib
- :inherited-members:
+Parsing
+-------
+
+.. autoclass:: fontTools.feaLib.parser.Parser
+ :members: parse
+ :member-order: bysource
+
+Building
+---------
+
+.. automodule:: fontTools.feaLib.builder
+ :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString
+
+Generation/Interrogation
+------------------------
+
+.. _`glyph-containing object`:
+.. _`glyph-containing objects`:
+
+In the below, a **glyph-containing object** is an object of one of the following
+classes: :class:`GlyphName`, :class:`GlyphClass`, :class:`GlyphClassName`.
+
+.. automodule:: fontTools.feaLib.ast
+ :member-order: bysource
:members:
- :undoc-members:
diff --git a/Doc/source/feaLib/lexer.rst b/Doc/source/feaLib/lexer.rst
deleted file mode 100644
index 939abf7e..00000000
--- a/Doc/source/feaLib/lexer.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-#####
-lexer
-#####
-
-.. automodule:: fontTools.feaLib.lexer
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/feaLib/parser.rst b/Doc/source/feaLib/parser.rst
deleted file mode 100644
index 2d5a6124..00000000
--- a/Doc/source/feaLib/parser.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-######
-parser
-######
-
-.. automodule:: fontTools.feaLib.parser
- :inherited-members:
- :members:
- :undoc-members:
diff --git a/Doc/source/index.rst b/Doc/source/index.rst
index 41f70c35..2162cc13 100644
--- a/Doc/source/index.rst
+++ b/Doc/source/index.rst
@@ -8,13 +8,12 @@
fontTools Docs
==============
-|Travis Build Status| |Appveyor Build status| |Coverage Status| |PyPI| |Gitter Chat|
-
About
-----
+fontTools is a family of libraries and utilities for manipulating fonts in Python.
-fontTools is a library for manipulating fonts, written in Python. The project includes the TTX tool, that can convert TrueType and OpenType fonts to and from an XML text format, which is also called TTX. It supports TrueType, OpenType, AFM and to an extent Type 1 and some Mac-specific formats. The project has an `MIT open-source license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. Among other things this means you can use it free of charge.
+The project has an `MIT open-source license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. Among other things this means you can use it free of charge.
Installation
------------
@@ -27,322 +26,80 @@ The package is listed in the Python Package Index (PyPI), so you can install it
pip install fonttools
-If you would like to contribute to its development, you can clone the repository from GitHub, install the package in 'editable' mode and modify the source code in place. We recommend creating a virtual environment, using the Python 3 `venv <https://docs.python.org/3/library/venv.html>`_ module::
-
- # download the source code to 'fonttools' folder
- git clone https://github.com/fonttools/fonttools.git
- cd fonttools
-
- # create new virtual environment called e.g. 'fonttools-venv', or anything you like
- python -m venv fonttools-venv
-
- # source the `activate` shell script to enter the environment (Un*x)
- . fonttools-venv/bin/activate
-
- # to activate the virtual environment in Windows `cmd.exe`, do
- fonttools-venv\Scripts\activate.bat
-
- # install in 'editable' mode
- pip install -e .
-
-
-.. note::
-
- To exit a Python virtual environment, enter the command ``deactivate``.
-
See the Optional Requirements section below for details about module-specific dependencies that must be installed in select cases.
-
-TTX – From OpenType and TrueType to XML and Back
-------------------------------------------------
-
-Once installed you can use the ttx command to convert binary font files (.otf, .ttf, etc) to the TTX XML format, edit them, and convert them back to binary format. TTX files have a .ttx file extension::
-
- ttx /path/to/font.otf
- ttx /path/to/font.ttx
-
-The TTX application can be used in two ways, depending on what platform you run it on:
-
-* As a command line tool (Windows/DOS, Unix, macOS)
-* By dropping files onto the application (Windows, macOS)
-
-TTX detects what kind of files it is fed: it will output a ``.ttx`` file when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or ``.otf`` when the input file is a ``.ttx`` file. By default, the output file is created in the same folder as the input file, and will have the same name as the input file but with a different extension. TTX will never overwrite existing files, but if necessary will append a unique number to the output filename (before the extension) such as ``Arial#1.ttf``.
-
-When using TTX from the command line there are a bunch of extra options. These are explained in the help text, as displayed when typing ``ttx -h`` at the command prompt. These additional options include:
-
-
-* specifying the folder where the output files are created
-* specifying which tables to dump or which tables to exclude
-* merging partial .ttx files with existing .ttf or .otf files
-* listing brief table info instead of dumping to .ttx
-* splitting tables to separate .ttx files
-* disabling TrueType instruction disassembly
-
-The TTX file format
-^^^^^^^^^^^^^^^^^^^
-
-The following tables are currently supported::
-
- BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
- Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH,
- MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1,
- TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX,
- VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm,
- fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar,
- loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop,
- sbix, trak, vhea and vmtx
-
-Other tables are dumped as hexadecimal data.
-
-TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most places. While this is fine in binary form, it is really hard to work with for humans. Therefore we use names instead.
-
-The glyph names are either extracted from the ``CFF`` table or the ``post`` table, or are derived from a Unicode ``cmap`` table. In the latter case the Adobe Glyph List is used to calculate names based on Unicode values. If all of these methods fail, names are invented based on GlyphID (eg ``glyph00142``)
-
-It is possible that different glyphs use the same name. If this happens, we force the names to be unique by appending #n to the name (n being an integer number.) The original names are being kept, so this has no influence on a "round tripped" font.
-
-Because the order in which glyphs are stored inside the binary font is important, we maintain an ordered list of glyph names in the font.
-
-Please see the :py:mod:`fontTools.ttx` documentation for additional details.
-
-
-Other Tools
------------
-
-Commands for merging and subsetting fonts are also available::
-
- pyftmerge
- pyftsubset
-
-Please see the :py:mod:`fontTools.merge` and :py:mod:`fontTools.subset` documentation for additional information about these tools.
-
-
-fontTools Python Library
-------------------------
-
-The fontTools Python library provides a convenient way to programmatically edit font files::
-
- >>> from fontTools.ttLib import TTFont
- >>> font = TTFont('/path/to/font.ttf')
- >>> font
- <fontTools.ttLib.TTFont object at 0x10c34ed50>
- >>>
-
-A selection of sample Python programs is in the `Snippets directory <https://github.com/fonttools/fonttools/blob/master/Snippets/>`_ of the fontTools repository.
-
-Please navigate to the respective area of the documentation to learn more about the available modules in the fontTools library.
-
-
-Optional Requirements
+Utilities
+---------
+
+fontTools installs four command-line utilities:
+
+- ``pyftmerge``, a tool for merging fonts; see :py:mod:`fontTools.merge`
+- ``pyftsubset``, a tool for subsetting fonts; see :py:mod:`fontTools.subset`
+- ``ttx``, a tool for converting between OpenType binary fonts (OTF) and an XML representation (TTX); see :py:mod:`fontTools.ttx`
+- ``fonttools``, a "meta-tool" for accessing other components of the fontTools family.
+
+This last utility takes a subcommand, which could be one of:
+
+- ``cffLib.width``: Calculate optimum defaultWidthX/nominalWidthX values
+- ``cu2qu``: Convert a UFO font from cubic to quadratic curves
+- ``feaLib``: Add features from a feature file (.fea) into a OTF font
+- ``help``: Show this help
+- ``merge``: Merge multiple fonts into one
+- ``mtiLib``: Convert a FontDame OTL file to TTX XML
+- ``subset``: OpenType font subsetter and optimizer
+- ``ttLib.woff2``: Compress and decompress WOFF2 fonts
+- ``ttx``: Convert OpenType fonts to XML and back
+- ``varLib``: Build a variable font from a designspace file and masters
+- ``varLib.instancer``: Partially instantiate a variable font.
+- ``varLib.interpolatable``: Test for interpolatability issues between fonts
+- ``varLib.interpolate_layout``: Interpolate GDEF/GPOS/GSUB tables for a point on a designspace
+- ``varLib.models``: Normalize locations on a given designspace
+- ``varLib.mutator``: Instantiate a variation font
+- ``varLib.varStore``: Optimize a font's GDEF variation store
+
+Libraries
+---------
+
+The main library you will want to access when using fontTools for font
+engineering is likely to be :py:mod:`fontTools.ttLib`, which is the package
+for handling TrueType/OpenType fonts. However, there are many other
+libraries in the fontTools suite:
+
+- :py:mod:`fontTools.afmLib`: Module for reading and writing AFM files
+- :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.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
+- :py:mod:`fontTools.feaLib`: Read and read AFDKO feature files
+- :py:mod:`fontTools.fontBuilder`: Construct TTF/OTF fonts from scratch
+- :py:mod:`fontTools.merge`: Tools for merging font files
+- :py:mod:`fontTools.pens`: Various classes for manipulating glyph outlines
+- :py:mod:`fontTools.subset`: OpenType font subsetting and optimization
+- :py:mod:`fontTools.svgLib.path`: Library for drawing SVG paths onto glyphs
+- :py:mod:`fontTools.t1Lib`: Tools for PostScript Type 1 fonts (Python2 only)
+- :py:mod:`fontTools.ttx`: Module for converting between OTF and XML representation
+- :py:mod:`fontTools.ufoLib`: Module for reading and writing UFO files
+- :py:mod:`fontTools.unicodedata`: Convert between Unicode and OpenType script information
+- :py:mod:`fontTools.varLib`: Module for dealing with 'gvar'-style font variations
+- :py:mod:`fontTools.voltLib`: Module for dealing with Visual OpenType Layout Tool (VOLT) files
+
+A selection of sample Python programs using these libaries can be found in the `Snippets directory <https://github.com/fonttools/fonttools/blob/master/Snippets/>`_ of the fontTools repository.
+
+Optional Dependencies
---------------------
The fontTools package currently has no (required) external dependencies
besides the modules included in the Python Standard Library.
However, a few extra dependencies are required to unlock optional features
-in some of the library modules.
-
-The fonttools PyPI distribution also supports so-called "extras", i.e. a
-set of keywords that describe a group of additional dependencies, which can be
-used when installing via pip, or when specifying a requirement.
-For example:
-
-.. code:: sh
-
- pip install fonttools[ufo,lxml,woff,unicode]
-
-This command will install fonttools, as well as the optional dependencies that
-are required to unlock the extra features named "ufo", etc.
-
-.. note::
-
- Optional dependencies are detailed by module in the list below with the ``Extra`` setting that automates ``pip`` dependency installation when this is supported.
-
-
-
-:py:mod:`fontTools.misc.etree`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-The module exports a ElementTree-like API for reading/writing XML files, and allows to use as the backend either the built-in ``xml.etree`` module or `lxml <https://lxml.de>`__. The latter is preferred whenever present, as it is generally faster and more secure.
-
-*Extra:* ``lxml``
-
-
-:py:mod:`fontTools.ufoLib`
-^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Package for reading and writing UFO source files; it requires:
-
-* `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem abstraction layer.
-
-* `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum`` module (only required on Python < 3.4).
-
-*Extra:* ``ufo``
-
+in some of the library modules. See the :doc:`optional requirements <./optional>`
+page for more information.
-:py:mod:`fontTools.ttLib.woff2`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Module to compress/decompress WOFF 2.0 web fonts; it requires:
-
-* `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of the Brotli compression library.
-
-*Extra:* ``woff``
-
-
-:py:mod:`fontTools.unicode`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To display the Unicode character names when dumping the ``cmap`` table
-with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
-The version included in there varies between different Python versions.
-To use the latest available data, you can install:
-
-* `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: ``unicodedata`` backport for Python 2.7
- and 3.x updated to the latest Unicode version 12.0. Note this is not necessary if you use Python 3.8
- as the latter already comes with an up-to-date ``unicodedata``.
-
-*Extra:* ``unicode``
-
-
-:py:mod:`fontTools.varLib.interpolatable`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Module for finding wrong contour/component order between different masters.
-It requires one of the following packages in order to solve the so-called
-"minimum weight perfect matching problem in bipartite graphs", or
-the Assignment problem:
-
-* `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library for Python, which internally
- uses `NumPy <https://pypi.python.org/pypi/numpy>`__ arrays and hence is very fast;
-* `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python module that implements the Hungarian
- or Kuhn-Munkres algorithm.
-
-*Extra:* ``interpolatable``
-
-
-:py:mod:`fontTools.varLib.plot`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Module for visualizing DesignSpaceDocument and resulting VariationModel.
-
-* `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
-
-*Extra:* ``plot``
-
-
-:py:mod:`fontTools.misc.symfont`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Advanced module for symbolic font statistics analysis; it requires:
-
-* `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for symbolic mathematics.
-
-*Extra:* ``symfont``
-
-
-:py:mod:`fontTools.t1Lib`
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-To get the file creator and type of Macintosh PostScript Type 1 fonts
-on Python 3 you need to install the following module, as the old ``MacOS``
-module is no longer included in Mac Python:
-
-* `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for extended filesystem attributes
- (macOS platform only).
-
-*Extra:* ``type1``
-
-
-:py:mod:`fontTools.pens.cocoaPen`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
-
-* `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between Python and the Objective-C
- runtime (macOS platform only).
-
-
-:py:mod:`fontTools.pens.qtPen`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
-
-* `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for the Qt cross platform UI and
- application toolkit.
-
-
-:py:mod:`fontTools.pens.reportLabPen`
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Pen to drawing glyphs as PNG images, requires:
-
-* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit for generating PDFs and
- graphics.
-
-
-Testing
--------
-
-To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
-When you run the ``pytest`` command, the tests will run against the
-installed fontTools package, or the first one found in the
-``PYTHONPATH``.
-
-You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
-automatically run tests on different Python versions in isolated virtual
-environments::
-
- pip install tox
- tox
-
-
-.. note::
-
- When you run ``tox`` without arguments, the tests are executed for all the environments listed in the ``tox.ini`` ``envlist``. The current Python interpreters defined for tox testing must be available on your system ``PATH``.
-
-You can specify a different testing environment list via the ``-e`` option, or the ``TOXENV`` environment variable::
-
- tox -e py36
- TOXENV="py36-cov,htmlcov" tox
-
-
-Development Community
+Developer information
---------------------
-fontTools development is ongoing in an active community of developers that includes professional developers employed at major software corporations and type foundries as well as hobbyists.
-
-Feature requests and bug reports are always welcome at https://github.com/fonttools/fonttools/issues/
-
-The best place for end-user and developer discussion about the fontTools project is the `fontTools gitter channel <https://gitter.im/fonttools-dev/Lobby>`_. There is also a development https://groups.google.com/d/forum/fonttools-dev mailing list for continuous integration notifications.
-
-
-History
--------
-
-The fontTools project was started by Just van Rossum in 1999, and was
-maintained as an open source project at
-http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
-began helping Just with stability maintenance. In 2013 Behdad Esfahbod
-began a friendly fork, thoroughly reviewing the codebase and making
-changes at https://github.com/behdad/fonttools to add new features and
-support for new font formats.
-
-
-Acknowledgments
----------------
-
-In alphabetical order:
-
-Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland,
-Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent
-Connare, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod,
-Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis
-Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson,
-Denis Moyogo Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek,
-Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo,
-Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret
-Rieger, Read Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel,
-Georg Seifert, Chris Simpkins, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov,
-Paul Wise.
+Information for developers can be found :doc:`here <./developer>`.
License
-------
diff --git a/Doc/source/merge.rst b/Doc/source/merge.rst
index 2fd85ef2..31146155 100644
--- a/Doc/source/merge.rst
+++ b/Doc/source/merge.rst
@@ -1,8 +1,10 @@
-#####
-merge
-#####
+####################################
+merge: Merge multiple fonts into one
+####################################
-.. automodule:: fontTools.merge
+``fontTools.merge`` provides both a library and a command line interface
+(``fonttools merge``) for merging multiple fonts together.
+
+.. autoclass:: fontTools.merge.Merger
:inherited-members:
:members:
- :undoc-members:
diff --git a/Doc/source/optional.rst b/Doc/source/optional.rst
new file mode 100644
index 00000000..09376a26
--- /dev/null
+++ b/Doc/source/optional.rst
@@ -0,0 +1,140 @@
+Optional Dependencies
+=====================
+
+The fonttools PyPI distribution also supports so-called "extras", i.e. a
+set of keywords that describe a group of additional dependencies, which can be
+used when installing via pip, or when specifying a requirement.
+For example:
+
+.. code:: sh
+
+ pip install fonttools[ufo,lxml,woff,unicode]
+
+This command will install fonttools, as well as the optional dependencies that
+are required to unlock the extra features named "ufo", etc.
+
+.. note::
+
+ Optional dependencies are detailed by module in the list below with the ``Extra`` setting that automates ``pip`` dependency installation when this is supported.
+
+
+
+:py:mod:`fontTools.misc.etree`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The module exports a ElementTree-like API for reading/writing XML files, and allows to use as the backend either the built-in ``xml.etree`` module or `lxml <https://lxml.de>`__. The latter is preferred whenever present, as it is generally faster and more secure.
+
+*Extra:* ``lxml``
+
+
+:py:mod:`fontTools.ufoLib`
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Package for reading and writing UFO source files; it requires:
+
+* `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem abstraction layer.
+
+* `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum`` module (only required on Python < 3.4).
+
+*Extra:* ``ufo``
+
+
+:py:mod:`fontTools.ttLib.woff2`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Module to compress/decompress WOFF 2.0 web fonts; it requires:
+
+* `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of the Brotli compression library.
+
+*Extra:* ``woff``
+
+
+:py:mod:`fontTools.unicode`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To display the Unicode character names when dumping the ``cmap`` table
+with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
+The version included in there varies between different Python versions.
+To use the latest available data, you can install:
+
+* `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: ``unicodedata`` backport for Python 2.7
+ and 3.x updated to the latest Unicode version 12.0. Note this is not necessary if you use Python 3.8
+ as the latter already comes with an up-to-date ``unicodedata``.
+
+*Extra:* ``unicode``
+
+
+:py:mod:`fontTools.varLib.interpolatable`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Module for finding wrong contour/component order between different masters.
+It requires one of the following packages in order to solve the so-called
+"minimum weight perfect matching problem in bipartite graphs", or
+the Assignment problem:
+
+* `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library for Python, which internally
+ uses `NumPy <https://pypi.python.org/pypi/numpy>`__ arrays and hence is very fast;
+* `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python module that implements the Hungarian
+ or Kuhn-Munkres algorithm.
+
+*Extra:* ``interpolatable``
+
+
+:py:mod:`fontTools.varLib.plot`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Module for visualizing DesignSpaceDocument and resulting VariationModel.
+
+* `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
+
+*Extra:* ``plot``
+
+
+:py:mod:`fontTools.misc.symfont`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Advanced module for symbolic font statistics analysis; it requires:
+
+* `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for symbolic mathematics.
+
+*Extra:* ``symfont``
+
+
+:py:mod:`fontTools.t1Lib`
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To get the file creator and type of Macintosh PostScript Type 1 fonts
+on Python 3 you need to install the following module, as the old ``MacOS``
+module is no longer included in Mac Python:
+
+* `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for extended filesystem attributes
+ (macOS platform only).
+
+*Extra:* ``type1``
+
+
+:py:mod:`fontTools.pens.cocoaPen`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
+
+* `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between Python and the Objective-C
+ runtime (macOS platform only).
+
+
+:py:mod:`fontTools.pens.qtPen`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
+
+* `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for the Qt cross platform UI and
+ application toolkit.
+
+
+:py:mod:`fontTools.pens.reportLabPen`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Pen to drawing glyphs as PNG images, requires:
+
+* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit for generating PDFs and
+ graphics.
diff --git a/Doc/source/ttx.rst b/Doc/source/ttx.rst
index bdc93858..d672bfc8 100644
--- a/Doc/source/ttx.rst
+++ b/Doc/source/ttx.rst
@@ -2,6 +2,60 @@
ttx
###
+
+TTX – From OpenType and TrueType to XML and Back
+------------------------------------------------
+
+Once installed you can use the ttx command to convert binary font files (.otf, .ttf, etc) to the TTX XML format, edit them, and convert them back to binary format. TTX files have a .ttx file extension::
+
+ ttx /path/to/font.otf
+ ttx /path/to/font.ttx
+
+The TTX application can be used in two ways, depending on what platform you run it on:
+
+* As a command line tool (Windows/DOS, Unix, macOS)
+* By dropping files onto the application (Windows, macOS)
+
+TTX detects what kind of files it is fed: it will output a ``.ttx`` file when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or ``.otf`` when the input file is a ``.ttx`` file. By default, the output file is created in the same folder as the input file, and will have the same name as the input file but with a different extension. TTX will never overwrite existing files, but if necessary will append a unique number to the output filename (before the extension) such as ``Arial#1.ttf``.
+
+When using TTX from the command line there are a bunch of extra options. These are explained in the help text, as displayed when typing ``ttx -h`` at the command prompt. These additional options include:
+
+
+* specifying the folder where the output files are created
+* specifying which tables to dump or which tables to exclude
+* merging partial .ttx files with existing .ttf or .otf files
+* listing brief table info instead of dumping to .ttx
+* splitting tables to separate .ttx files
+* disabling TrueType instruction disassembly
+
+The TTX file format
+^^^^^^^^^^^^^^^^^^^
+
+.. begin table list
+
+The following tables are currently supported::
+
+ BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
+ Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH,
+ MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1,
+ TSI2, TSI3, TSI5, TSIB, TSIC, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA,
+ VDMX, VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat,
+ fpgm, fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern,
+ lcar, loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep,
+ prop, sbix, trak, vhea and vmtx
+
+.. end table list
+
+Other tables are dumped as hexadecimal data.
+
+TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most places. While this is fine in binary form, it is really hard to work with for humans. Therefore we use names instead.
+
+The glyph names are either extracted from the ``CFF`` table or the ``post`` table, or are derived from a Unicode ``cmap`` table. In the latter case the Adobe Glyph List is used to calculate names based on Unicode values. If all of these methods fail, names are invented based on GlyphID (eg ``glyph00142``)
+
+It is possible that different glyphs use the same name. If this happens, we force the names to be unique by appending #n to the name (n being an integer number.) The original names are being kept, so this has no influence on a "round tripped" font.
+
+Because the order in which glyphs are stored inside the binary font is important, we maintain an ordered list of glyph names in the font.
+
.. automodule:: fontTools.ttx
:inherited-members:
:members:
diff --git a/LICENSE.external b/LICENSE.external
index 88a0272f..2bc4dab3 100644
--- a/LICENSE.external
+++ b/LICENSE.external
@@ -26,6 +26,10 @@ XITS font project
This Font Software is licensed under the SIL Open Font License, Version 1.1.
+Iosevka
+ Copyright (c) 2015-2020 Belleve Invis (belleve@typeof.net).
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
+
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py
index dd6ffca1..11cc487d 100644
--- a/Lib/fontTools/__init__.py
+++ b/Lib/fontTools/__init__.py
@@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.9.0"
+version = __version__ = "4.10.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/Lib/fontTools/__main__.py b/Lib/fontTools/__main__.py
index ba09c7fc..9b978aaa 100644
--- a/Lib/fontTools/__main__.py
+++ b/Lib/fontTools/__main__.py
@@ -5,8 +5,6 @@ def main(args=None):
if args is None:
args = sys.argv[1:]
- # TODO Add help output, --help, etc.
-
# TODO Handle library-wide options. Eg.:
# --unicodedata
# --verbose / other logging stuff
@@ -20,6 +18,10 @@ def main(args=None):
# can be added. Should we just try importing the fonttools
# module first and try without if it fails?
+ if len(sys.argv) < 2:
+ sys.argv.append("help")
+ if sys.argv[1] == "-h" or sys.argv[1] == "--help":
+ sys.argv[1] = "help"
mod = 'fontTools.'+sys.argv[1]
sys.argv[1] = sys.argv[0] + ' ' + sys.argv[1]
del sys.argv[0]
diff --git a/Lib/fontTools/afmLib.py b/Lib/fontTools/afmLib.py
index 9b0c9e4f..7983fa09 100644
--- a/Lib/fontTools/afmLib.py
+++ b/Lib/fontTools/afmLib.py
@@ -1,8 +1,50 @@
-"""Module for reading and writing AFM files."""
+"""Module for reading and writing AFM (Adobe Font Metrics) files.
+
+Note that this has been designed to read in AFM files generated by Fontographer
+and has not been tested on many other files. In particular, it does not
+implement the whole Adobe AFM specification [#f1]_ but, it should read most
+"common" AFM files.
+
+Here is an example of using `afmLib` to read, modify and write an AFM file:
+
+ >>> from fontTools.afmLib import AFM
+ >>> f = AFM("Tests/afmLib/data/TestAFM.afm")
+ >>>
+ >>> # Accessing a pair gets you the kern value
+ >>> f[("V","A")]
+ -60
+ >>>
+ >>> # Accessing a glyph name gets you metrics
+ >>> f["A"]
+ (65, 668, (8, -25, 660, 666))
+ >>> # (charnum, width, bounding box)
+ >>>
+ >>> # Accessing an attribute gets you metadata
+ >>> f.FontName
+ 'TestFont-Regular'
+ >>> f.FamilyName
+ 'TestFont'
+ >>> f.Weight
+ 'Regular'
+ >>> f.XHeight
+ 500
+ >>> f.Ascender
+ 750
+ >>>
+ >>> # Attributes and items can also be set
+ >>> f[("A","V")] = -150 # Tighten kerning
+ >>> f.FontName = "TestFont Squished"
+ >>>
+ >>> # And the font written out again
+ >>> f.write("testfont-squished.afm")
+
+.. rubric:: Footnotes
+
+.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
+ Adobe Font Metrics File Format Specification.
+
+"""
-# XXX reads AFM's generated by Fog, not tested with much else.
-# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics
-# File Format Specification). Still, it should read most "common" AFM files.
from fontTools.misc.py23 import *
import re
@@ -97,6 +139,11 @@ class AFM(object):
]
def __init__(self, path=None):
+ """AFM file reader.
+
+ Instantiating an object with a path name will cause the file to be opened,
+ read, and parsed. Alternatively the path can be left unspecified, and a
+ file can be parsed later with the :meth:`read` method."""
self._attrs = {}
self._chars = {}
self._kerning = {}
@@ -107,6 +154,7 @@ class AFM(object):
self.read(path)
def read(self, path):
+ """Opens, reads and parses a file."""
lines = readlines(path)
for line in lines:
if not line.strip():
@@ -189,6 +237,7 @@ class AFM(object):
self._composites[charname] = components
def write(self, path, sep='\r'):
+ """Writes out an AFM font to the given path."""
import time
lines = [ "StartFontMetrics 2.0",
"Comment Generated by afmLib; at %s" % (
@@ -258,24 +307,40 @@ class AFM(object):
writelines(path, lines, sep)
def has_kernpair(self, pair):
+ """Returns `True` if the given glyph pair (specified as a tuple) exists
+ in the kerning dictionary."""
return pair in self._kerning
def kernpairs(self):
+ """Returns a list of all kern pairs in the kerning dictionary."""
return list(self._kerning.keys())
def has_char(self, char):
+ """Returns `True` if the given glyph exists in the font."""
return char in self._chars
def chars(self):
+ """Returns a list of all glyph names in the font."""
return list(self._chars.keys())
def comments(self):
+ """Returns all comments from the file."""
return self._comments
def addComment(self, comment):
+ """Adds a new comment to the file."""
self._comments.append(comment)
def addComposite(self, glyphName, components):
+ """Specifies that the glyph `glyphName` is made up of the given components.
+ The components list should be of the following form::
+
+ [
+ (glyphname, xOffset, yOffset),
+ ...
+ ]
+
+ """
self._composites[glyphName] = components
def __getattr__(self, attr):
diff --git a/Lib/fontTools/agl.py b/Lib/fontTools/agl.py
index e47112aa..b7d0bfa3 100644
--- a/Lib/fontTools/agl.py
+++ b/Lib/fontTools/agl.py
@@ -3,6 +3,28 @@
# https://github.com/adobe-type-tools/agl-aglfn/raw/4036a9ca80a62f64f9de4f7321a9a045ad0ecfd6/glyphlist.txt
# and
# https://github.com/adobe-type-tools/agl-aglfn/raw/4036a9ca80a62f64f9de4f7321a9a045ad0ecfd6/aglfn.txt
+"""
+Interface to the Adobe Glyph List
+
+This module exists to convert glyph names from the Adobe Glyph List
+to their Unicode equivalents. Example usage:
+
+ >>> from fontTools.agl import toUnicode
+ >>> toUnicode("nahiragana")
+ 'な'
+
+It also contains two dictionaries, ``UV2AGL`` and ``AGL2UV``, which map from
+Unicode codepoints to AGL names and vice versa:
+
+ >>> import fontTools
+ >>> fontTools.agl.UV2AGL[ord("?")]
+ 'question'
+ >>> fontTools.agl.AGL2UV["wcircumflex"]
+ 373
+
+This is used by fontTools when it has to construct glyph names for a font which
+doesn't include any (e.g. format 3.0 post tables).
+"""
from fontTools.misc.py23 import *
import re
@@ -5083,9 +5105,9 @@ _builddicts()
def toUnicode(glyph, isZapfDingbats=False):
- """Convert glyph names to Unicode, such as 'longs_t.oldstyle' --> u'ſt'
+ """Convert glyph names to Unicode, such as ``'longs_t.oldstyle'`` --> ``u'ſt'``
- If isZapfDingbats is True, the implementation recognizes additional
+ If ``isZapfDingbats`` is ``True``, the implementation recognizes additional
glyph names (as required by the AGL specification).
"""
# https://github.com/adobe-type-tools/agl-specification#2-the-mapping
diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py
index d67bc13e..e97b7501 100644
--- a/Lib/fontTools/cffLib/__init__.py
+++ b/Lib/fontTools/cffLib/__init__.py
@@ -1,4 +1,15 @@
-"""cffLib.py -- read/write tools for Adobe CFF fonts."""
+"""cffLib: read/write Adobe CFF fonts
+
+OpenType fonts with PostScript outlines contain a completely independent
+font file, Adobe's *Compact Font Format*. So dealing with OpenType fonts
+requires also dealing with CFF. This module allows you to read and write
+fonts written in the CFF format.
+
+In 2016, OpenType 1.8 introduced the `CFF2 <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2>`_
+format which, along with other changes, extended the CFF format to deal with
+the demands of variable fonts. This module parses both original CFF and CFF2.
+
+"""
from fontTools.misc.py23 import *
from fontTools.misc import sstruct
@@ -28,8 +39,37 @@ maxStackLimit = 513
class CFFFontSet(object):
+ """A CFF font "file" can contain more than one font, although this is
+ extremely rare (and not allowed within OpenType fonts).
+
+ This class is the entry point for parsing a CFF table. To actually
+ manipulate the data inside the CFF font, you will want to access the
+ ``CFFFontSet``'s :class:`TopDict` object. To do this, a ``CFFFontSet``
+ object can either be treated as a dictionary (with appropriate
+ ``keys()`` and ``values()`` methods) mapping font names to :class:`TopDict`
+ objects, or as a list.
+
+ .. code:: python
+
+ from fontTools import ttLib
+ tt = ttLib.TTFont("Tests/cffLib/data/LinLibertine_RBI.otf")
+ tt["CFF "].cff
+ # <fontTools.cffLib.CFFFontSet object at 0x101e24c90>
+ tt["CFF "].cff[0] # Here's your actual font data
+ # <fontTools.cffLib.TopDict object at 0x1020f1fd0>
+
+ """
def decompile(self, file, otFont, isCFF2=None):
+ """Parse a binary CFF file into an internal representation. ``file``
+ should be a file handle object. ``otFont`` is the top-level
+ :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file.
+
+ If ``isCFF2`` is passed and set to ``True`` or ``False``, then the
+ library makes an assertion that the CFF header is of the appropriate
+ version.
+ """
+
self.otFont = otFont
sstruct.unpack(cffHeaderFormat, file.read(3), self)
if isCFF2 is not None:
@@ -89,6 +129,14 @@ class CFFFontSet(object):
return self.topDictIndex[index]
def compile(self, file, otFont, isCFF2=None):
+ """Write the object back into binary representation onto the given file.
+ ``file`` should be a file handle object. ``otFont`` is the top-level
+ :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file.
+
+ If ``isCFF2`` is passed and set to ``True`` or ``False``, then the
+ library makes an assertion that the CFF header is of the appropriate
+ version.
+ """
self.otFont = otFont
if isCFF2 is not None:
# called from ttLib: assert 'major' value matches expected version
@@ -144,6 +192,16 @@ class CFFFontSet(object):
writer.toFile(file)
def toXML(self, xmlWriter):
+ """Write the object into XML representation onto the given
+ :class:`fontTools.misc.xmlWriter.XMLWriter`.
+
+ .. code:: python
+
+ writer = xmlWriter.XMLWriter(sys.stdout)
+ tt["CFF "].cff.toXML(writer)
+
+ """
+
xmlWriter.simpletag("major", value=self.major)
xmlWriter.newline()
xmlWriter.simpletag("minor", value=self.minor)
@@ -163,6 +221,7 @@ class CFFFontSet(object):
xmlWriter.newline()
def fromXML(self, name, attrs, content, otFont=None):
+ """Reads data from the XML element into the ``CFFFontSet`` object."""
self.otFont = otFont
# set defaults. These will be replaced if there are entries for them
@@ -230,7 +289,11 @@ class CFFFontSet(object):
self.minor = int(attrs['value'])
def convertCFFToCFF2(self, otFont):
- # This assumes a decompiled CFF table.
+ """Converts this object from CFF format to CFF2 format. This conversion
+ is done 'in-place'. The conversion cannot be reversed.
+
+ This assumes a decompiled CFF table. (i.e. that the object has been
+ filled via :meth:`decompile`.)"""
self.major = 2
cff2GetGlyphOrder = self.otFont.getGlyphOrder
topDictData = TopDictIndex(None, cff2GetGlyphOrder, None)
@@ -307,7 +370,8 @@ class CFFFontSet(object):
class CFFWriter(object):
-
+ """Helper class for serializing CFF data to binary. Used by
+ :meth:`CFFFontSet.compile`."""
def __init__(self, isCFF2):
self.data = []
self.isCFF2 = isCFF2
@@ -367,6 +431,8 @@ def calcOffSize(largestOffset):
class IndexCompiler(object):
+ """Base class for writing CFF `INDEX data <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#5-index-data>`_
+ to binary."""
def __init__(self, items, strings, parent, isCFF2=None):
if isCFF2 is None and hasattr(parent, "isCFF2"):
@@ -446,6 +512,7 @@ class IndexedStringsCompiler(IndexCompiler):
class TopDictIndexCompiler(IndexCompiler):
+ """Helper class for writing the TopDict to binary."""
def getItems(self, items, strings):
out = []
@@ -481,6 +548,9 @@ class TopDictIndexCompiler(IndexCompiler):
class FDArrayIndexCompiler(IndexCompiler):
+ """Helper class for writing the
+ `Font DICT INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect>`_
+ to binary."""
def getItems(self, items, strings):
out = []
@@ -519,6 +589,8 @@ class FDArrayIndexCompiler(IndexCompiler):
class GlobalSubrsCompiler(IndexCompiler):
+ """Helper class for writing the `global subroutine INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#9-local-and-global-subr-indexes>`_
+ to binary."""
def getItems(self, items, strings):
out = []
@@ -529,14 +601,17 @@ class GlobalSubrsCompiler(IndexCompiler):
class SubrsCompiler(GlobalSubrsCompiler):
-
+ """Helper class for writing the `local subroutine INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#9-local-and-global-subr-indexes>`_
+ to binary."""
+
def setPos(self, pos, endPos):
offset = pos - self.parent.pos
self.parent.rawDict["Subrs"] = offset
class CharStringsCompiler(GlobalSubrsCompiler):
-
+ """Helper class for writing the `CharStrings INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#9-local-and-global-subr-indexes>`_
+ to binary."""
def getItems(self, items, strings):
out = []
for cs in items:
@@ -549,8 +624,9 @@ class CharStringsCompiler(GlobalSubrsCompiler):
class Index(object):
-
- """This class represents what the CFF spec calls an INDEX."""
+ """This class represents what the CFF spec calls an INDEX (an array of
+ variable-sized objects). `Index` items can be addressed and set using
+ Python list indexing."""
compilerClass = IndexCompiler
@@ -608,16 +684,50 @@ class Index(object):
return data
def append(self, item):
+ """Add an item to an INDEX."""
self.items.append(item)
def getCompiler(self, strings, parent, isCFF2=None):
return self.compilerClass(self, strings, parent, isCFF2=isCFF2)
def clear(self):
+ """Empty the INDEX."""
del self.items[:]
class GlobalSubrsIndex(Index):
+ """This index contains all the global subroutines in the font. A global
+ subroutine is a set of ``CharString`` data which is accessible to any
+ glyph in the font, and are used to store repeated instructions - for
+ example, components may be encoded as global subroutines, but so could
+ hinting instructions.
+
+ Remember that when interpreting a ``callgsubr`` instruction (or indeed
+ a ``callsubr`` instruction) that you will need to add the "subroutine
+ number bias" to number given:
+
+ .. code:: python
+
+ tt = ttLib.TTFont("Almendra-Bold.otf")
+ u = tt["CFF "].cff[0].CharStrings["udieresis"]
+ u.decompile()
+
+ u.toXML(XMLWriter(sys.stdout))
+ # <some stuff>
+ # -64 callgsubr <-- Subroutine which implements the dieresis mark
+ # <other stuff>
+
+ tt["CFF "].cff[0].GlobalSubrs[-64] # <-- WRONG
+ # <T2CharString (bytecode) at 103451d10>
+
+ tt["CFF "].cff[0].GlobalSubrs[-64 + 107] # <-- RIGHT
+ # <T2CharString (source) at 103451390>
+
+ ("The bias applied depends on the number of subrs (gsubrs). If the number of
+ subrs (gsubrs) is less than 1240, the bias is 107. Otherwise if it is less
+ than 33900, it is 1131; otherwise it is 32768.",
+ `Subroutine Operators <https://docs.microsoft.com/en-us/typography/opentype/otspec180/cff2charstr#section4.4>`)
+ """
compilerClass = GlobalSubrsCompiler
subrClass = psCharStrings.T2CharString
@@ -647,6 +757,15 @@ class GlobalSubrsIndex(Index):
return self.subrClass(data, private=private, globalSubrs=self.globalSubrs)
def toXML(self, xmlWriter):
+ """Write the subroutines index into XML representation onto the given
+ :class:`fontTools.misc.xmlWriter.XMLWriter`.
+
+ .. code:: python
+
+ writer = xmlWriter.XMLWriter(sys.stdout)
+ tt["CFF "].cff[0].GlobalSubrs.toXML(writer)
+
+ """
xmlWriter.comment(
"The 'index' attribute is only for humans; "
"it is ignored when parsed.")
@@ -677,10 +796,26 @@ class GlobalSubrsIndex(Index):
class SubrsIndex(GlobalSubrsIndex):
+ """This index contains a glyph's local subroutines. A local subroutine is a
+ private set of ``CharString`` data which is accessible only to the glyph to
+ which the index is attached."""
+
compilerClass = SubrsCompiler
class TopDictIndex(Index):
+ """This index represents the array of ``TopDict`` structures in the font
+ (again, usually only one entry is present). Hence the following calls are
+ equivalent:
+
+ .. code:: python
+
+ tt["CFF "].cff[0]
+ # <fontTools.cffLib.TopDict object at 0x102ed6e50>
+ tt["CFF "].cff.topDictIndex[0]
+ # <fontTools.cffLib.TopDict object at 0x102ed6e50>
+
+ """
compilerClass = TopDictIndexCompiler
@@ -869,6 +1004,20 @@ class FDSelect(object):
class CharStrings(object):
+ """The ``CharStrings`` in the font represent the instructions for drawing
+ each glyph. This object presents a dictionary interface to the font's
+ CharStrings, indexed by glyph name:
+
+ .. code:: python
+
+ tt["CFF "].cff[0].CharStrings["a"]
+ # <T2CharString (bytecode) at 103451e90>
+
+ See :class:`fontTools.misc.psCharStrings.T1CharString` and
+ :class:`fontTools.misc.psCharStrings.T2CharString` for how to decompile,
+ compile and interpret the glyph drawing instructions in the returned objects.
+
+ """
def __init__(self, file, charset, globalSubrs, private, fdSelect, fdArray,
isCFF2=None):
@@ -2087,27 +2236,33 @@ class DictCompiler(object):
def arg_delta_blend(self, value):
- """ A delta list with blend lists has to be *all* blend lists.
- The value is a list is arranged as follows.
- [
- [V0, d0..dn]
- [V1, d0..dn]
- ...
- [Vm, d0..dn]
- ]
- V is the absolute coordinate value from the default font, and d0-dn are
- the delta values from the n regions. Each V is an absolute coordinate
- from the default font.
- We want to return a list:
- [
- [v0, v1..vm]
- [d0..dn]
- ...
- [d0..dn]
- numBlends
- blendOp
- ]
- where each v is relative to the previous default font value.
+ """A delta list with blend lists has to be *all* blend lists.
+
+ The value is a list is arranged as follows::
+
+ [
+ [V0, d0..dn]
+ [V1, d0..dn]
+ ...
+ [Vm, d0..dn]
+ ]
+
+ ``V`` is the absolute coordinate value from the default font, and ``d0-dn``
+ are the delta values from the *n* regions. Each ``V`` is an absolute
+ coordinate from the default font.
+
+ We want to return a list::
+
+ [
+ [v0, v1..vm]
+ [d0..dn]
+ ...
+ [d0..dn]
+ numBlends
+ blendOp
+ ]
+
+ where each ``v`` is relative to the previous default font value.
"""
numMasters = len(value[0])
numBlends = len(value)
@@ -2356,6 +2511,30 @@ class BaseDict(object):
class TopDict(BaseDict):
+ """The ``TopDict`` represents the top-level dictionary holding font
+ information. CFF2 tables contain a restricted set of top-level entries
+ as described `here <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#7-top-dict-data>`_,
+ but CFF tables may contain a wider range of information. This information
+ can be accessed through attributes or through the dictionary returned
+ through the ``rawDict`` property:
+
+ .. code:: python
+
+ font = tt["CFF "].cff[0]
+ font.FamilyName
+ # 'Linux Libertine O'
+ font.rawDict["FamilyName"]
+ # 'Linux Libertine O'
+
+ More information is available in the CFF file's private dictionary, accessed
+ via the ``Private`` property:
+
+ .. code:: python
+
+ tt["CFF "].cff[0].Private.BlueValues
+ # [-15, 0, 515, 515, 666, 666]
+
+ """
defaults = buildDefaults(topDictOperators)
converters = buildConverters(topDictOperators)
@@ -2377,6 +2556,7 @@ class TopDict(BaseDict):
self.order = buildOrder(topDictOperators)
def getGlyphOrder(self):
+ """Returns a list of glyph names in the CFF font."""
return self.charset
def postDecompile(self):
diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py
index ff2c9ae8..1d2f4b73 100644
--- a/Lib/fontTools/cffLib/specializer.py
+++ b/Lib/fontTools/cffLib/specializer.py
@@ -1,6 +1,17 @@
# -*- coding: utf-8 -*-
-"""T2CharString operator specializer and generalizer."""
+"""T2CharString operator specializer and generalizer.
+
+PostScript glyph drawing operations can be expressed in multiple different
+ways. For example, as well as the ``lineto`` operator, there is also a
+``hlineto`` operator which draws a horizontal line, removing the need to
+specify a ``dx`` coordinate, and a ``vlineto`` operator which draws a
+vertical line, removing the need to specify a ``dy`` coordinate. As well
+as decompiling :class:`fontTools.misc.psCharStrings.T2CharString` objects
+into lists of operations, this module allows for conversion between general
+and specific forms of the operation.
+
+"""
from fontTools.misc.py23 import *
from fontTools.cffLib import maxStackLimit
diff --git a/Lib/fontTools/cffLib/width.py b/Lib/fontTools/cffLib/width.py
index d959da1c..edce446f 100644
--- a/Lib/fontTools/cffLib/width.py
+++ b/Lib/fontTools/cffLib/width.py
@@ -1,6 +1,11 @@
# -*- coding: utf-8 -*-
-"""T2CharString glyph width optimizer."""
+"""T2CharString glyph width optimizer.
+
+CFF glyphs whose width equals the CFF Private dictionary's ``defaultWidthX``
+value do not need to specify their width in their charstring, saving bytes.
+This module determines the optimum ``defaultWidthX`` and ``nominalWidthX``
+values for a font, when provided with a list of glyph widths."""
from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont, getTableClass
@@ -146,17 +151,34 @@ def optimizeWidths(widths):
return default, nominal
+def main(args=None):
+ """Calculate optimum defaultWidthX/nominalWidthX values"""
+
+ import argparse
+ parser = argparse.ArgumentParser(
+ "fonttools cffLib.width",
+ description=main.__doc__,
+ )
+ parser.add_argument('inputs', metavar='FILE', type=str, nargs='+',
+ help="Input TTF files")
+ parser.add_argument('-b', '--brute-force', dest="brute", action="store_true",
+ help="Use brute-force approach (VERY slow)")
+
+ args = parser.parse_args(args)
+
+ for fontfile in args.inputs:
+ font = TTFont(fontfile)
+ hmtx = font['hmtx']
+ widths = [m[0] for m in hmtx.metrics.values()]
+ if args.brute:
+ default, nominal = optimizeWidthsBruteforce(widths)
+ else:
+ default, nominal = optimizeWidths(widths)
+ print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal)))
if __name__ == '__main__':
import sys
if len(sys.argv) == 1:
import doctest
sys.exit(doctest.testmod().failed)
- for fontfile in sys.argv[1:]:
- font = TTFont(fontfile)
- hmtx = font['hmtx']
- widths = [m[0] for m in hmtx.metrics.values()]
- default, nominal = optimizeWidths(widths)
- print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal)))
- #default, nominal = optimizeWidthsBruteforce(widths)
- #print("glyphs=%d default=%d nominal=%d byteCost=%d" % (len(widths), default, nominal, byteCost(widths, default, nominal)))
+ main()
diff --git a/Lib/fontTools/colorLib/builder.py b/Lib/fontTools/colorLib/builder.py
index 9fe7f203..dae00b91 100644
--- a/Lib/fontTools/colorLib/builder.py
+++ b/Lib/fontTools/colorLib/builder.py
@@ -1,3 +1,7 @@
+"""
+colorLib.builder: Build COLR/CPAL tables from scratch
+
+"""
import collections
import copy
import enum
diff --git a/Lib/fontTools/cu2qu/cli.py b/Lib/fontTools/cu2qu/cli.py
index 7f3c1ef7..d4e83b88 100644
--- a/Lib/fontTools/cu2qu/cli.py
+++ b/Lib/fontTools/cu2qu/cli.py
@@ -59,6 +59,7 @@ def _copytree(input_path, output_path):
def main(args=None):
+ """Convert a UFO font from cubic to quadratic curves"""
parser = argparse.ArgumentParser(prog="cu2qu")
parser.add_argument(
"--version", action="version", version=fontTools.__version__)
diff --git a/Lib/fontTools/cu2qu/cu2qu.py b/Lib/fontTools/cu2qu/cu2qu.py
index 1fdd5e24..c9ce93ae 100644
--- a/Lib/fontTools/cu2qu/cu2qu.py
+++ b/Lib/fontTools/cu2qu/cu2qu.py
@@ -46,7 +46,15 @@ else:
@cython.returns(cython.double)
@cython.locals(v1=cython.complex, v2=cython.complex)
def dot(v1, v2):
- """Return the dot product of two vectors."""
+ """Return the dot product of two vectors.
+
+ Args:
+ v1 (complex): First vector.
+ v2 (complex): Second vector.
+
+ Returns:
+ double: Dot product.
+ """
return (v1 * v2.conjugate()).real
@@ -77,6 +85,21 @@ def calc_cubic_parameters(p0, p1, p2, p3):
@cython.cfunc
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
def split_cubic_into_n_iter(p0, p1, p2, p3, n):
+ """Split a cubic Bezier into n equal parts.
+
+ Splits the curve into `n` equal parts by curve time.
+ (t=0..1/n, t=1/n..2/n, ...)
+
+ Args:
+ p0 (complex): Start point of curve.
+ p1 (complex): First handle of curve.
+ p2 (complex): Second handle of curve.
+ p3 (complex): End point of curve.
+
+ Returns:
+ An iterator yielding the control points (four complex values) of the
+ subcurves.
+ """
# Hand-coded special-cases
if n == 2:
return iter(split_cubic_into_two(p0, p1, p2, p3))
@@ -115,6 +138,20 @@ def _split_cubic_into_n_gen(p0, p1, p2, p3, n):
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(mid=cython.complex, deriv3=cython.complex)
def split_cubic_into_two(p0, p1, p2, p3):
+ """Split a cubic Bezier into two equal parts.
+
+ Splits the curve into two equal parts at t = 0.5
+
+ Args:
+ p0 (complex): Start point of curve.
+ p1 (complex): First handle of curve.
+ p2 (complex): Second handle of curve.
+ p3 (complex): End point of curve.
+
+ Returns:
+ tuple: Two cubic Beziers (each expressed as a tuple of four complex
+ values).
+ """
mid = (p0 + 3 * (p1 + p2) + p3) * .125
deriv3 = (p3 + p2 - p1 - p0) * .125
return ((p0, (p0 + p1) * .5, mid - deriv3, mid),
@@ -124,6 +161,20 @@ def split_cubic_into_two(p0, p1, p2, p3):
@cython.locals(p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex, _27=cython.double)
@cython.locals(mid1=cython.complex, deriv1=cython.complex, mid2=cython.complex, deriv2=cython.complex)
def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
+ """Split a cubic Bezier into three equal parts.
+
+ Splits the curve into three equal parts at t = 1/3 and t = 2/3
+
+ Args:
+ p0 (complex): Start point of curve.
+ p1 (complex): First handle of curve.
+ p2 (complex): Second handle of curve.
+ p3 (complex): End point of curve.
+
+ Returns:
+ tuple: Three cubic Beziers (each expressed as a tuple of four complex
+ values).
+ """
# we define 1/27 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function
mid1 = (8*p0 + 12*p1 + 6*p2 + p3) * _27
@@ -139,8 +190,18 @@ def split_cubic_into_three(p0, p1, p2, p3, _27=1/27):
@cython.locals(t=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(_p1=cython.complex, _p2=cython.complex)
def cubic_approx_control(t, p0, p1, p2, p3):
- """Approximate a cubic bezier curve with a quadratic one.
- Returns the candidate control point."""
+ """Approximate a cubic Bezier using a quadratic one.
+
+ Args:
+ t (double): Position of control point.
+ p0 (complex): Start point of curve.
+ p1 (complex): First handle of curve.
+ p2 (complex): Second handle of curve.
+ p3 (complex): End point of curve.
+
+ Returns:
+ complex: Location of candidate control point on quadratic curve.
+ """
_p1 = p0 + (p1 - p0) * 1.5
_p2 = p3 + (p2 - p3) * 1.5
return _p1 + (_p2 - _p1) * t
@@ -150,8 +211,18 @@ def cubic_approx_control(t, p0, p1, p2, p3):
@cython.locals(a=cython.complex, b=cython.complex, c=cython.complex, d=cython.complex)
@cython.locals(ab=cython.complex, cd=cython.complex, p=cython.complex, h=cython.double)
def calc_intersect(a, b, c, d):
- """Calculate the intersection of ab and cd, given a, b, c, d."""
+ """Calculate the intersection of two lines.
+
+ Args:
+ a (complex): Start point of first line.
+ b (complex): End point of first line.
+ c (complex): Start point of second line.
+ d (complex): End point of second line.
+ Returns:
+ complex: Location of intersection if one present, ``complex(NaN,NaN)``
+ if no intersection was found.
+ """
ab = b - a
cd = d - c
p = ab * 1j
@@ -167,10 +238,23 @@ def calc_intersect(a, b, c, d):
@cython.locals(tolerance=cython.double, p0=cython.complex, p1=cython.complex, p2=cython.complex, p3=cython.complex)
@cython.locals(mid=cython.complex, deriv3=cython.complex)
def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
- """Returns True if the cubic Bezier p entirely lies within a distance
- tolerance of origin, False otherwise. Assumes that p0 and p3 do fit
- within tolerance of origin, and just checks the inside of the curve."""
-
+ """Check if a cubic Bezier lies within a given distance of the origin.
+
+ "Origin" means *the* origin (0,0), not the start of the curve. Note that no
+ checks are made on the start and end positions of the curve; this function
+ only checks the inside of the curve.
+
+ Args:
+ p0 (complex): Start point of curve.
+ p1 (complex): First handle of curve.
+ p2 (complex): Second handle of curve.
+ p3 (complex): End point of curve.
+ tolerance (double): Distance from origin.
+
+ Returns:
+ bool: True if the cubic Bezier ``p`` entirely lies within a distance
+ ``tolerance`` of the origin, False otherwise.
+ """
# First check p2 then p1, as p2 has higher error early on.
if abs(p2) <= tolerance and abs(p1) <= tolerance:
return True
@@ -188,8 +272,18 @@ def cubic_farthest_fit_inside(p0, p1, p2, p3, tolerance):
@cython.locals(tolerance=cython.double, _2_3=cython.double)
@cython.locals(q1=cython.complex, c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
- """Return the uniq quadratic approximating cubic that maintains
- endpoint tangents if that is within tolerance, None otherwise."""
+ """Approximate a cubic Bezier with a single quadratic within a given tolerance.
+
+ Args:
+ cubic (sequence): Four complex numbers representing control points of
+ the cubic Bezier curve.
+ tolerance (double): Permitted deviation from the original curve.
+
+ Returns:
+ Three complex numbers representing control points of the quadratic
+ curve if it fits within the given tolerance, or ``None`` if no suitable
+ curve could be calculated.
+ """
# we define 2/3 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function
@@ -214,10 +308,18 @@ def cubic_approx_quadratic(cubic, tolerance, _2_3=2/3):
@cython.locals(c0=cython.complex, c1=cython.complex, c2=cython.complex, c3=cython.complex)
@cython.locals(q0=cython.complex, q1=cython.complex, next_q1=cython.complex, q2=cython.complex, d1=cython.complex)
def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
- """Approximate a cubic bezier curve with a spline of n quadratics.
-
- Returns None if no quadratic approximation is found which lies entirely
- within a distance `tolerance` from the original curve.
+ """Approximate a cubic Bezier curve with a spline of n quadratics.
+
+ Args:
+ cubic (sequence): Four complex numbers representing control points of
+ the cubic Bezier curve.
+ n (int): Number of quadratic Bezier curves in the spline.
+ tolerance (double): Permitted deviation from the original curve.
+
+ Returns:
+ A list of ``n+2`` complex numbers, representing control points of the
+ quadratic spline if it fits within the given tolerance, or ``None`` if
+ no suitable spline could be calculated.
"""
# we define 2/3 as a keyword argument so that it will be evaluated only
# once but still in the scope of this function
@@ -268,9 +370,17 @@ def cubic_approx_spline(cubic, n, tolerance, _2_3=2/3):
@cython.locals(max_err=cython.double)
@cython.locals(n=cython.int)
def curve_to_quadratic(curve, max_err):
- """Return a quadratic spline approximating this cubic bezier.
- Raise 'ApproxNotFoundError' if no suitable approximation can be found
- with the given parameters.
+ """Approximate a cubic Bezier curve with a spline of n quadratics.
+
+ Args:
+ cubic (sequence): Four 2D tuples representing control points of
+ the cubic Bezier curve.
+ max_err (double): Permitted deviation from the original curve.
+
+ Returns:
+ A list of 2D tuples, representing control points of the quadratic
+ spline if it fits within the given tolerance, or ``None`` if no
+ suitable spline could be calculated.
"""
curve = [complex(*p) for p in curve]
@@ -287,9 +397,33 @@ def curve_to_quadratic(curve, max_err):
@cython.locals(l=cython.int, last_i=cython.int, i=cython.int)
def curves_to_quadratic(curves, max_errors):
- """Return quadratic splines approximating these cubic beziers.
- Raise 'ApproxNotFoundError' if no suitable approximation can be found
- for all curves with the given parameters.
+ """Return quadratic Bezier splines approximating the input cubic Beziers.
+
+ Args:
+ curves: A sequence of *n* curves, each curve being a sequence of four
+ 2D tuples.
+ max_errors: A sequence of *n* floats representing the maximum permissible
+ deviation from each of the cubic Bezier curves.
+
+ Example::
+
+ >>> curves_to_quadratic( [
+ ... [ (50,50), (100,100), (150,100), (200,50) ],
+ ... [ (75,50), (120,100), (150,75), (200,60) ]
+ ... ], [1,1] )
+ [[(50.0, 50.0), (75.0, 75.0), (125.0, 91.66666666666666), (175.0, 75.0), (200.0, 50.0)], [(75.0, 50.0), (97.5, 75.0), (135.41666666666666, 82.08333333333333), (175.0, 67.5), (200.0, 60.0)]]
+
+ The returned splines have "implied oncurve points" suitable for use in
+ TrueType ``glif`` outlines - i.e. in the first spline returned above,
+ the first quadratic segment runs from (50,50) to
+ ( (75 + 125)/2 , (120 + 91.666..)/2 ) = (100, 83.333...).
+
+ Returns:
+ A list of splines, each spline being a list of 2D tuples.
+
+ Raises:
+ fontTools.cu2qu.Errors.ApproxNotFoundError: if no suitable approximation
+ can be found for all curves with the given parameters.
"""
curves = [[complex(*p) for p in curve] for curve in curves]
diff --git a/Lib/fontTools/cu2qu/errors.py b/Lib/fontTools/cu2qu/errors.py
index d3adfea4..74c4c227 100644
--- a/Lib/fontTools/cu2qu/errors.py
+++ b/Lib/fontTools/cu2qu/errors.py
@@ -19,7 +19,7 @@ class Error(Exception):
class ApproxNotFoundError(Error):
def __init__(self, curve):
message = "no approximation found: %s" % curve
- super(Error, self).__init__(message)
+ super().__init__(message)
self.curve = curve
diff --git a/Lib/fontTools/feaLib/__main__.py b/Lib/fontTools/feaLib/__main__.py
index b69307e8..e7db157f 100644
--- a/Lib/fontTools/feaLib/__main__.py
+++ b/Lib/fontTools/feaLib/__main__.py
@@ -13,6 +13,7 @@ log = logging.getLogger("fontTools.feaLib")
def main(args=None):
+ """Add features from a feature file (.fea) into a OTF font"""
parser = argparse.ArgumentParser(
description="Use fontTools to compile OpenType feature files (*.fea).")
parser.add_argument(
diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py
index e416c0c6..a305e67f 100644
--- a/Lib/fontTools/feaLib/ast.py
+++ b/Lib/fontTools/feaLib/ast.py
@@ -7,33 +7,36 @@ import itertools
SHIFT = " " * 4
__all__ = [
+ 'Element',
+ 'FeatureFile',
+ 'Comment',
+ 'GlyphName',
+ 'GlyphClass',
+ 'GlyphClassName',
+ 'MarkClassName',
+ 'AnonymousBlock',
+ 'Block',
+ 'FeatureBlock',
+ 'NestedBlock',
+ 'LookupBlock',
+ 'GlyphClassDefinition',
+ 'GlyphClassDefStatement',
+ 'MarkClass',
+ 'MarkClassDefinition',
'AlternateSubstStatement',
'Anchor',
'AnchorDefinition',
- 'AnonymousBlock',
'AttachStatement',
'BaseAxis',
- 'Block',
- 'BytesIO',
'CVParametersNameStatement',
'ChainContextPosStatement',
'ChainContextSubstStatement',
'CharacterStatement',
- 'Comment',
'CursivePosStatement',
- 'Element',
'Expression',
- 'FeatureBlock',
- 'FeatureFile',
- 'FeatureLibError',
'FeatureNameStatement',
'FeatureReferenceStatement',
'FontRevisionStatement',
- 'GlyphClass',
- 'GlyphClassDefStatement',
- 'GlyphClassDefinition',
- 'GlyphClassName',
- 'GlyphName',
'HheaField',
'IgnorePosStatement',
'IgnoreSubstStatement',
@@ -43,34 +46,23 @@ __all__ = [
'LigatureCaretByIndexStatement',
'LigatureCaretByPosStatement',
'LigatureSubstStatement',
- 'LookupBlock',
'LookupFlagStatement',
'LookupReferenceStatement',
'MarkBasePosStatement',
- 'MarkClass',
- 'MarkClassDefinition',
- 'MarkClassName',
'MarkLigPosStatement',
'MarkMarkPosStatement',
'MultipleSubstStatement',
'NameRecord',
- 'NestedBlock',
'OS2Field',
- 'OrderedDict',
'PairPosStatement',
- 'Py23Error',
'ReverseChainSingleSubstStatement',
'ScriptStatement',
- 'SimpleNamespace',
'SinglePosStatement',
'SingleSubstStatement',
'SizeParameters',
'Statement',
- 'StringIO',
'SubtableStatement',
'TableBlock',
- 'Tag',
- 'UnicodeIO',
'ValueRecord',
'ValueRecordDefinition',
'VheaField',
@@ -117,14 +109,19 @@ def asFea(g):
class Element(object):
+ """A base class representing "something" in a feature file."""
def __init__(self, location=None):
+ #: location of this element - tuple of ``(filename, line, column)``
self.location = location
def build(self, builder):
pass
def asFea(self, indent=""):
+ """Returns this element as a string of feature code. For block-type
+ elements (such as :class:`FeatureBlock`), the `indent` string is
+ added to the start of each line in the output."""
raise NotImplementedError
def __str__(self):
@@ -140,8 +137,10 @@ class Expression(Element):
class Comment(Element):
+ """A comment in a feature file."""
def __init__(self, text, location=None):
super(Comment, self).__init__(location)
+ #: Text of the comment
self.text = text
def asFea(self, indent=""):
@@ -149,12 +148,14 @@ class Comment(Element):
class GlyphName(Expression):
- """A single glyph name, such as cedilla."""
+ """A single glyph name, such as ``cedilla``."""
def __init__(self, glyph, location=None):
Expression.__init__(self, location)
+ #: The name itself as a string
self.glyph = glyph
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return (self.glyph,)
def asFea(self, indent=""):
@@ -162,14 +163,16 @@ class GlyphName(Expression):
class GlyphClass(Expression):
- """A glyph class, such as [acute cedilla grave]."""
+ """A glyph class, such as ``[acute cedilla grave]``."""
def __init__(self, glyphs=None, location=None):
Expression.__init__(self, location)
+ #: The list of glyphs in this class, as :class:`GlyphName` objects.
self.glyphs = glyphs if glyphs is not None else []
self.original = []
self.curr = 0
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return tuple(self.glyphs)
def asFea(self, indent=""):
@@ -182,12 +185,18 @@ class GlyphClass(Expression):
return "[" + " ".join(map(asFea, self.glyphs)) + "]"
def extend(self, glyphs):
+ """Add a list of :class:`GlyphName` objects to the class."""
self.glyphs.extend(glyphs)
def append(self, glyph):
+ """Add a single :class:`GlyphName` object to the class."""
self.glyphs.append(glyph)
def add_range(self, start, end, glyphs):
+ """Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end``
+ are either :class:`GlyphName` objects or strings representing the
+ start and end glyphs in the class, and ``glyphs`` is the full list of
+ :class:`GlyphName` objects in the range."""
if self.curr < len(self.glyphs):
self.original.extend(self.glyphs[self.curr:])
self.original.append((start, end))
@@ -195,6 +204,9 @@ class GlyphClass(Expression):
self.curr = len(self.glyphs)
def add_cid_range(self, start, end, glyphs):
+ """Add a range to the class by glyph ID. ``start`` and ``end`` are the
+ initial and final IDs, and ``glyphs`` is the full list of
+ :class:`GlyphName` objects in the range."""
if self.curr < len(self.glyphs):
self.original.extend(self.glyphs[self.curr:])
self.original.append(("\\{}".format(start), "\\{}".format(end)))
@@ -202,6 +214,8 @@ class GlyphClass(Expression):
self.curr = len(self.glyphs)
def add_class(self, gc):
+ """Add glyphs from the given :class:`GlyphClassName` object to the
+ class."""
if self.curr < len(self.glyphs):
self.original.extend(self.glyphs[self.curr:])
self.original.append(gc)
@@ -210,13 +224,15 @@ class GlyphClass(Expression):
class GlyphClassName(Expression):
- """A glyph class name, such as @FRENCH_MARKS."""
+ """A glyph class name, such as ``@FRENCH_MARKS``. This must be instantiated
+ with a :class:`GlyphClassDefinition` object."""
def __init__(self, glyphclass, location=None):
Expression.__init__(self, location)
assert isinstance(glyphclass, GlyphClassDefinition)
self.glyphclass = glyphclass
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return tuple(self.glyphclass.glyphSet())
def asFea(self, indent=""):
@@ -224,13 +240,15 @@ class GlyphClassName(Expression):
class MarkClassName(Expression):
- """A mark class name, such as @FRENCH_MARKS defined with markClass."""
+ """A mark class name, such as ``@FRENCH_MARKS`` defined with ``markClass``.
+ This must be instantiated with a :class:`MarkClass` object."""
def __init__(self, markClass, location=None):
Expression.__init__(self, location)
assert isinstance(markClass, MarkClass)
self.markClass = markClass
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return self.markClass.glyphSet()
def asFea(self, indent=""):
@@ -238,9 +256,12 @@ class MarkClassName(Expression):
class AnonymousBlock(Statement):
+ """An anonymous data block."""
+
def __init__(self, tag, content, location=None):
Statement.__init__(self, location)
- self.tag, self.content = tag, content
+ self.tag = tag #: string containing the block's "tag"
+ self.content = content #: block data as string
def asFea(self, indent=""):
res = "anon {} {{\n".format(self.tag)
@@ -250,11 +271,15 @@ class AnonymousBlock(Statement):
class Block(Statement):
+ """A block of statements: feature, lookup, etc."""
def __init__(self, location=None):
Statement.__init__(self, location)
- self.statements = []
+ self.statements = [] #: Statements contained in the block
def build(self, builder):
+ """When handed a 'builder' object of comparable interface to
+ :class:`fontTools.feaLib.builder`, walks the statements in this
+ block, calling the builder callbacks."""
for s in self.statements:
s.build(builder)
@@ -265,6 +290,8 @@ class Block(Statement):
class FeatureFile(Block):
+ """The top-level element of the syntax tree, containing the whole feature
+ file in its ``statements`` attribute."""
def __init__(self):
Block.__init__(self, location=None)
self.markClasses = {} # name --> ast.MarkClass
@@ -274,11 +301,14 @@ class FeatureFile(Block):
class FeatureBlock(Block):
+ """A named feature block."""
def __init__(self, name, use_extension=False, location=None):
Block.__init__(self, location)
self.name, self.use_extension = name, use_extension
def build(self, builder):
+ """Call the ``start_feature`` callback on the builder object, visit
+ all the statements in this feature, and then call ``end_feature``."""
# TODO(sascha): Handle use_extension.
builder.start_feature(self.location, self.name)
# language exclude_dflt statements modify builder.features_
@@ -302,6 +332,8 @@ class FeatureBlock(Block):
class NestedBlock(Block):
+ """A block inside another block, for example when found inside a
+ ``cvParameters`` block."""
def __init__(self, tag, block_name, location=None):
Block.__init__(self, location)
self.tag = tag
@@ -320,6 +352,7 @@ class NestedBlock(Block):
class LookupBlock(Block):
+ """A named lookup, containing ``statements``."""
def __init__(self, name, use_extension=False, location=None):
Block.__init__(self, location)
self.name, self.use_extension = name, use_extension
@@ -341,6 +374,7 @@ class LookupBlock(Block):
class TableBlock(Block):
+ """A ``table ... { }`` block."""
def __init__(self, name, location=None):
Block.__init__(self, location)
self.name = name
@@ -353,13 +387,14 @@ class TableBlock(Block):
class GlyphClassDefinition(Statement):
- """Example: @UPPERCASE = [A-Z];"""
+ """Example: ``@UPPERCASE = [A-Z];``."""
def __init__(self, name, glyphs, location=None):
Statement.__init__(self, location)
- self.name = name
- self.glyphs = glyphs
+ self.name = name #: class name as a string, without initial ``@``
+ self.glyphs = glyphs #: a :class:`GlyphClass` object
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return tuple(self.glyphs.glyphSet())
def asFea(self, indent=""):
@@ -367,7 +402,9 @@ class GlyphClassDefinition(Statement):
class GlyphClassDefStatement(Statement):
- """Example: GlyphClassDef @UPPERCASE, [B], [C], [D];"""
+ """Example: ``GlyphClassDef @UPPERCASE, [B], [C], [D];``. The parameters
+ must be either :class:`GlyphClass` or :class:`GlyphClassName` objects, or
+ ``None``."""
def __init__(self, baseGlyphs, markGlyphs, ligatureGlyphs,
componentGlyphs, location=None):
Statement.__init__(self, location)
@@ -376,6 +413,7 @@ class GlyphClassDefStatement(Statement):
self.componentGlyphs = componentGlyphs
def build(self, builder):
+ """Calls the builder's ``add_glyphClassDef`` callback."""
base = self.baseGlyphs.glyphSet() if self.baseGlyphs else tuple()
liga = self.ligatureGlyphs.glyphSet() \
if self.ligatureGlyphs else tuple()
@@ -392,19 +430,28 @@ class GlyphClassDefStatement(Statement):
self.componentGlyphs.asFea() if self.componentGlyphs else "")
-# While glyph classes can be defined only once, the feature file format
-# allows expanding mark classes with multiple definitions, each using
-# different glyphs and anchors. The following are two MarkClassDefinitions
-# for the same MarkClass:
-# markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
-# markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
class MarkClass(object):
+ """One `or more` ``markClass`` statements for the same mark class.
+
+ While glyph classes can be defined only once, the feature file format
+ allows expanding mark classes with multiple definitions, each using
+ different glyphs and anchors. The following are two ``MarkClassDefinitions``
+ for the same ``MarkClass``::
+
+ markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
+ markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
+
+ The ``MarkClass`` object is therefore just a container for a list of
+ :class:`MarkClassDefinition` statements.
+ """
+
def __init__(self, name):
self.name = name
self.definitions = []
self.glyphs = OrderedDict() # glyph --> ast.MarkClassDefinitions
def addDefinition(self, definition):
+ """Add a :class:`MarkClassDefinition` statement to this mark class."""
assert isinstance(definition, MarkClassDefinition)
self.definitions.append(definition)
for glyph in definition.glyphSet():
@@ -421,6 +468,7 @@ class MarkClass(object):
self.glyphs[glyph] = definition
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return tuple(self.glyphs.keys())
def asFea(self, indent=""):
@@ -429,6 +477,27 @@ class MarkClass(object):
class MarkClassDefinition(Statement):
+ """A single ``markClass`` statement. The ``markClass`` should be a
+ :class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object,
+ and the ``glyphs`` parameter should be a `glyph-containing object`_ .
+
+ Example:
+
+ .. code:: python
+
+ mc = MarkClass("FRENCH_ACCENTS")
+ mc.addDefinition( MarkClassDefinition(mc, Anchor(350, 800),
+ GlyphClass([ GlyphName("acute"), GlyphName("grave") ])
+ ) )
+ mc.addDefinition( MarkClassDefinition(mc, Anchor(350, -200),
+ GlyphClass([ GlyphName("cedilla") ])
+ ) )
+
+ mc.asFea()
+ # markClass [acute grave] <anchor 350 800> @FRENCH_ACCENTS;
+ # markClass [cedilla] <anchor 350 -200> @FRENCH_ACCENTS;
+
+ """
def __init__(self, markClass, anchor, glyphs, location=None):
Statement.__init__(self, location)
assert isinstance(markClass, MarkClass)
@@ -436,6 +505,7 @@ class MarkClassDefinition(Statement):
self.markClass, self.anchor, self.glyphs = markClass, anchor, glyphs
def glyphSet(self):
+ """The glyphs in this class as a tuple of :class:`GlyphName` objects."""
return self.glyphs.glyphSet()
def asFea(self, indent=""):
@@ -445,12 +515,18 @@ class MarkClassDefinition(Statement):
class AlternateSubstStatement(Statement):
+ """A ``sub ... from ...`` statement.
+
+ ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
+ `glyph-containing objects`_. ``glyph`` should be a `one element list`."""
+
def __init__(self, prefix, glyph, suffix, replacement, location=None):
Statement.__init__(self, location)
self.prefix, self.glyph, self.suffix = (prefix, glyph, suffix)
self.replacement = replacement
def build(self, builder):
+ """Calls the builder's ``add_alternate_subst`` callback."""
glyph = self.glyph.glyphSet()
assert len(glyph) == 1, glyph
glyph = list(glyph)[0]
@@ -477,6 +553,11 @@ class AlternateSubstStatement(Statement):
class Anchor(Expression):
+ """An ``Anchor`` element, used inside a ``pos`` rule.
+
+ If a ``name`` is given, this will be used in preference to the coordinates.
+ Other values should be integer.
+ """
def __init__(self, x, y, name=None, contourpoint=None,
xDeviceTable=None, yDeviceTable=None, location=None):
Expression.__init__(self, location)
@@ -500,6 +581,7 @@ class Anchor(Expression):
class AnchorDefinition(Statement):
+ """A named anchor definition. (2.e.viii). ``name`` should be a string."""
def __init__(self, name, x, y, contourpoint=None, location=None):
Statement.__init__(self, location)
self.name, self.x, self.y, self.contourpoint = name, x, y, contourpoint
@@ -513,11 +595,14 @@ class AnchorDefinition(Statement):
class AttachStatement(Statement):
+ """A ``GDEF`` table ``Attach`` statement."""
def __init__(self, glyphs, contourPoints, location=None):
Statement.__init__(self, location)
- self.glyphs, self.contourPoints = (glyphs, contourPoints)
+ self.glyphs = glyphs #: A `glyph-containing object`_
+ self.contourPoints = contourPoints #: A list of integer contour points
def build(self, builder):
+ """Calls the builder's ``add_attach_points`` callback."""
glyphs = self.glyphs.glyphSet()
builder.add_attach_points(self.location, glyphs, self.contourPoints)
@@ -527,12 +612,24 @@ class AttachStatement(Statement):
class ChainContextPosStatement(Statement):
+ """A chained contextual positioning statement.
+
+ ``prefix``, ``glyphs``, and ``suffix`` should be lists of
+ `glyph-containing objects`_ .
+
+ ``lookups`` should be a list of lists containing :class:`LookupBlock`
+ statements. The length of the outer list should equal to the length of
+ ``glyphs``; the inner lists can be of variable length. Where there is no
+ chaining lookup at the given glyph position, the entry in ``lookups``
+ should be ``None``."""
+
def __init__(self, prefix, glyphs, suffix, lookups, location=None):
Statement.__init__(self, location)
self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
self.lookups = lookups
def build(self, builder):
+ """Calls the builder's ``add_chain_context_pos`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
glyphs = [g.glyphSet() for g in self.glyphs]
suffix = [s.glyphSet() for s in self.suffix]
@@ -546,8 +643,9 @@ class ChainContextPosStatement(Statement):
res += " ".join(g.asFea() for g in self.prefix) + " "
for i, g in enumerate(self.glyphs):
res += g.asFea() + "'"
- if self.lookups[i] is not None:
- res += " lookup " + self.lookups[i].name
+ if self.lookups[i]:
+ for lu in self.lookups[i]:
+ res += " lookup " + lu.name
if i < len(self.glyphs) - 1:
res += " "
if len(self.suffix):
@@ -559,12 +657,22 @@ class ChainContextPosStatement(Statement):
class ChainContextSubstStatement(Statement):
+ """A chained contextual substitution statement.
+
+ ``prefix``, ``glyphs``, and ``suffix`` should be lists of
+ `glyph-containing objects`_ .
+
+ ``lookups`` should be a list of :class:`LookupBlock` statements, with
+ length equal to the length of ``glyphs``. Where there is no chaining
+ lookup at the given glyph position, the entry in ``lookups`` should be
+ ``None``."""
def __init__(self, prefix, glyphs, suffix, lookups, location=None):
Statement.__init__(self, location)
self.prefix, self.glyphs, self.suffix = prefix, glyphs, suffix
self.lookups = lookups
def build(self, builder):
+ """Calls the builder's ``add_chain_context_subst`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
glyphs = [g.glyphSet() for g in self.glyphs]
suffix = [s.glyphSet() for s in self.suffix]
@@ -578,8 +686,9 @@ class ChainContextSubstStatement(Statement):
res += " ".join(g.asFea() for g in self.prefix) + " "
for i, g in enumerate(self.glyphs):
res += g.asFea() + "'"
- if self.lookups[i] is not None:
- res += " lookup " + self.lookups[i].name
+ if self.lookups[i]:
+ for lu in self.lookups[i]:
+ res += " lookup " + lu.name
if i < len(self.glyphs) - 1:
res += " "
if len(self.suffix):
@@ -591,12 +700,15 @@ class ChainContextSubstStatement(Statement):
class CursivePosStatement(Statement):
+ """A cursive positioning statement. Entry and exit anchors can either
+ be :class:`Anchor` objects or ``None``."""
def __init__(self, glyphclass, entryAnchor, exitAnchor, location=None):
Statement.__init__(self, location)
self.glyphclass = glyphclass
self.entryAnchor, self.exitAnchor = entryAnchor, exitAnchor
def build(self, builder):
+ """Calls the builder object's ``add_cursive_pos`` callback."""
builder.add_cursive_pos(
self.location, self.glyphclass.glyphSet(), self.entryAnchor, self.exitAnchor)
@@ -607,12 +719,13 @@ class CursivePosStatement(Statement):
class FeatureReferenceStatement(Statement):
- """Example: feature salt;"""
+ """Example: ``feature salt;``"""
def __init__(self, featureName, location=None):
Statement.__init__(self, location)
self.location, self.featureName = (location, featureName)
def build(self, builder):
+ """Calls the builder object's ``add_feature_reference`` callback."""
builder.add_feature_reference(self.location, self.featureName)
def asFea(self, indent=""):
@@ -620,11 +733,19 @@ class FeatureReferenceStatement(Statement):
class IgnorePosStatement(Statement):
+ """An ``ignore pos`` statement, containing `one or more` contexts to ignore.
+
+ ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
+ with each of ``prefix``, ``glyphs`` and ``suffix`` being
+ `glyph-containing objects`_ ."""
+
def __init__(self, chainContexts, location=None):
Statement.__init__(self, location)
self.chainContexts = chainContexts
def build(self, builder):
+ """Calls the builder object's ``add_chain_context_pos`` callback on each
+ rule context."""
for prefix, glyphs, suffix in self.chainContexts:
prefix = [p.glyphSet() for p in prefix]
glyphs = [g.glyphSet() for g in glyphs]
@@ -649,11 +770,18 @@ class IgnorePosStatement(Statement):
class IgnoreSubstStatement(Statement):
+ """An ``ignore sub`` statement, containing `one or more` contexts to ignore.
+
+ ``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
+ with each of ``prefix``, ``glyphs`` and ``suffix`` being
+ `glyph-containing objects`_ ."""
def __init__(self, chainContexts, location=None):
Statement.__init__(self, location)
self.chainContexts = chainContexts
def build(self, builder):
+ """Calls the builder object's ``add_chain_context_subst`` callback on
+ each rule context."""
for prefix, glyphs, suffix in self.chainContexts:
prefix = [p.glyphSet() for p in prefix]
glyphs = [g.glyphSet() for g in glyphs]
@@ -678,9 +806,10 @@ class IgnoreSubstStatement(Statement):
class IncludeStatement(Statement):
+ """An ``include()`` statement."""
def __init__(self, filename, location=None):
super(IncludeStatement, self).__init__(location)
- self.filename = filename
+ self.filename = filename #: String containing name of file to include
def build(self):
# TODO: consider lazy-loading the including parser/lexer?
@@ -694,15 +823,17 @@ class IncludeStatement(Statement):
class LanguageStatement(Statement):
+ """A ``language`` statement within a feature."""
def __init__(self, language, include_default=True, required=False,
location=None):
Statement.__init__(self, location)
assert(len(language) == 4)
- self.language = language
- self.include_default = include_default
+ self.language = language #: A four-character language tag
+ self.include_default = include_default #: If false, "exclude_dflt"
self.required = required
def build(self, builder):
+ """Call the builder object's ``set_language`` callback."""
builder.set_language(location=self.location, language=self.language,
include_default=self.include_default,
required=self.required)
@@ -718,11 +849,13 @@ class LanguageStatement(Statement):
class LanguageSystemStatement(Statement):
+ """A top-level ``languagesystem`` statement."""
def __init__(self, script, language, location=None):
Statement.__init__(self, location)
self.script, self.language = (script, language)
def build(self, builder):
+ """Calls the builder object's ``add_language_system`` callback."""
builder.add_language_system(self.location, self.script, self.language)
def asFea(self, indent=""):
@@ -730,6 +863,8 @@ class LanguageSystemStatement(Statement):
class FontRevisionStatement(Statement):
+ """A ``head`` table ``FontRevision`` statement. ``revision`` should be a
+ number, and will be formatted to three significant decimal places."""
def __init__(self, revision, location=None):
Statement.__init__(self, location)
self.revision = revision
@@ -742,11 +877,14 @@ class FontRevisionStatement(Statement):
class LigatureCaretByIndexStatement(Statement):
+ """A ``GDEF`` table ``LigatureCaretByIndex`` statement. ``glyphs`` should be
+ a `glyph-containing object`_, and ``carets`` should be a list of integers."""
def __init__(self, glyphs, carets, location=None):
Statement.__init__(self, location)
self.glyphs, self.carets = (glyphs, carets)
def build(self, builder):
+ """Calls the builder object's ``add_ligatureCaretByIndex_`` callback."""
glyphs = self.glyphs.glyphSet()
builder.add_ligatureCaretByIndex_(self.location, glyphs, set(self.carets))
@@ -756,11 +894,14 @@ class LigatureCaretByIndexStatement(Statement):
class LigatureCaretByPosStatement(Statement):
+ """A ``GDEF`` table ``LigatureCaretByPos`` statement. ``glyphs`` should be
+ a `glyph-containing object`_, and ``carets`` should be a list of integers."""
def __init__(self, glyphs, carets, location=None):
Statement.__init__(self, location)
self.glyphs, self.carets = (glyphs, carets)
def build(self, builder):
+ """Calls the builder object's ``add_ligatureCaretByPos_`` callback."""
glyphs = self.glyphs.glyphSet()
builder.add_ligatureCaretByPos_(self.location, glyphs, set(self.carets))
@@ -770,6 +911,14 @@ class LigatureCaretByPosStatement(Statement):
class LigatureSubstStatement(Statement):
+ """A chained contextual substitution statement.
+
+ ``prefix``, ``glyphs``, and ``suffix`` should be lists of
+ `glyph-containing objects`_; ``replacement`` should be a single
+ `glyph-containing object`_.
+
+ If ``forceChain`` is True, this is expressed as a chaining rule
+ (e.g. ``sub f' i' by f_i``) even when no context is given."""
def __init__(self, prefix, glyphs, suffix, replacement,
forceChain, location=None):
Statement.__init__(self, location)
@@ -801,6 +950,10 @@ class LigatureSubstStatement(Statement):
class LookupFlagStatement(Statement):
+ """A ``lookupflag`` statement. The ``value`` should be an integer value
+ representing the flags in use, but not including the ``markAttachment``
+ class and ``markFilteringSet`` values, which must be specified as
+ glyph-containing objects."""
def __init__(self, value=0, markAttachment=None, markFilteringSet=None,
location=None):
Statement.__init__(self, location)
@@ -809,6 +962,7 @@ class LookupFlagStatement(Statement):
self.markFilteringSet = markFilteringSet
def build(self, builder):
+ """Calls the builder object's ``set_lookup_flag`` callback."""
markAttach = None
if self.markAttachment is not None:
markAttach = self.markAttachment.glyphSet()
@@ -836,11 +990,15 @@ class LookupFlagStatement(Statement):
class LookupReferenceStatement(Statement):
+ """Represents a ``lookup ...;`` statement to include a lookup in a feature.
+
+ The ``lookup`` should be a :class:`LookupBlock` object."""
def __init__(self, lookup, location=None):
Statement.__init__(self, location)
self.location, self.lookup = (location, lookup)
def build(self, builder):
+ """Calls the builder object's ``add_lookup_call`` callback."""
builder.add_lookup_call(self.lookup.name)
def asFea(self, indent=""):
@@ -848,11 +1006,15 @@ class LookupReferenceStatement(Statement):
class MarkBasePosStatement(Statement):
+ """A mark-to-base positioning rule. The ``base`` should be a
+ `glyph-containing object`_. The ``marks`` should be a list of
+ (:class:`Anchor`, :class:`MarkClass`) tuples."""
def __init__(self, base, marks, location=None):
Statement.__init__(self, location)
self.base, self.marks = base, marks
def build(self, builder):
+ """Calls the builder object's ``add_mark_base_pos`` callback."""
builder.add_mark_base_pos(self.location, self.base.glyphSet(), self.marks)
def asFea(self, indent=""):
@@ -864,11 +1026,38 @@ class MarkBasePosStatement(Statement):
class MarkLigPosStatement(Statement):
+ """A mark-to-ligature positioning rule. The ``ligatures`` must be a
+ `glyph-containing object`_. The ``marks`` should be a list of lists: each
+ element in the top-level list represents a component glyph, and is made
+ up of a list of (:class:`Anchor`, :class:`MarkClass`) tuples representing
+ mark attachment points for that position.
+
+ Example::
+
+ m1 = MarkClass("TOP_MARKS")
+ m2 = MarkClass("BOTTOM_MARKS")
+ # ... add definitions to mark classes...
+
+ glyph = GlyphName("lam_meem_jeem")
+ marks = [
+ [ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
+ [ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
+ [ ] # No attachments on the jeem
+ ]
+ mlp = MarkLigPosStatement(glyph, marks)
+
+ mlp.asFea()
+ # pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS
+ # ligComponent <anchor 376 -378> mark @BOTTOM_MARKS;
+
+ """
+
def __init__(self, ligatures, marks, location=None):
Statement.__init__(self, location)
self.ligatures, self.marks = ligatures, marks
def build(self, builder):
+ """Calls the builder object's ``add_mark_lig_pos`` callback."""
builder.add_mark_lig_pos(self.location, self.ligatures.glyphSet(), self.marks)
def asFea(self, indent=""):
@@ -888,11 +1077,15 @@ class MarkLigPosStatement(Statement):
class MarkMarkPosStatement(Statement):
+ """A mark-to-mark positioning rule. The ``baseMarks`` must be a
+ `glyph-containing object`_. The ``marks`` should be a list of
+ (:class:`Anchor`, :class:`MarkClass`) tuples."""
def __init__(self, baseMarks, marks, location=None):
Statement.__init__(self, location)
self.baseMarks, self.marks = baseMarks, marks
def build(self, builder):
+ """Calls the builder object's ``add_mark_mark_pos`` callback."""
builder.add_mark_mark_pos(self.location, self.baseMarks.glyphSet(), self.marks)
def asFea(self, indent=""):
@@ -904,6 +1097,13 @@ class MarkMarkPosStatement(Statement):
class MultipleSubstStatement(Statement):
+ """A multiple substitution statement.
+
+ ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of
+ `glyph-containing objects`_.
+
+ If ``forceChain`` is True, this is expressed as a chaining rule
+ (e.g. ``sub f' i' by f_i``) even when no context is given."""
def __init__(
self, prefix, glyph, suffix, replacement, forceChain=False, location=None
):
@@ -913,6 +1113,7 @@ class MultipleSubstStatement(Statement):
self.forceChain = forceChain
def build(self, builder):
+ """Calls the builder object's ``add_multiple_subst`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
builder.add_multiple_subst(
@@ -936,6 +1137,14 @@ class MultipleSubstStatement(Statement):
class PairPosStatement(Statement):
+ """A pair positioning statement.
+
+ ``glyphs1`` and ``glyphs2`` should be `glyph-containing objects`_.
+ ``valuerecord1`` should be a :class:`ValueRecord` object;
+ ``valuerecord2`` should be either a :class:`ValueRecord` object or ``None``.
+ If ``enumerated`` is true, then this is expressed as an
+ `enumerated pair <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_.
+ """
def __init__(self, glyphs1, valuerecord1, glyphs2, valuerecord2,
enumerated=False, location=None):
Statement.__init__(self, location)
@@ -944,6 +1153,14 @@ class PairPosStatement(Statement):
self.glyphs2, self.valuerecord2 = glyphs2, valuerecord2
def build(self, builder):
+ """Calls a callback on the builder object:
+
+ * If the rule is enumerated, calls ``add_specific_pair_pos`` on each
+ combination of first and second glyphs.
+ * If the glyphs are both single :class:`GlyphName` objects, calls
+ ``add_specific_pair_pos``.
+ * Else, calls ``add_class_pair_pos``.
+ """
if self.enumerated:
g = [self.glyphs1.glyphSet(), self.glyphs2.glyphSet()]
for glyph1, glyph2 in itertools.product(*g):
@@ -977,6 +1194,13 @@ class PairPosStatement(Statement):
class ReverseChainSingleSubstStatement(Statement):
+ """A reverse chaining substitution statement. You don't see those every day.
+
+ Note the unusual argument order: ``suffix`` comes `before` ``glyphs``.
+ ``old_prefix``, ``old_suffix``, ``glyphs`` and ``replacements`` should be
+ lists of `glyph-containing objects`_. ``glyphs`` and ``replacements`` should
+ be one-item lists.
+ """
def __init__(self, old_prefix, old_suffix, glyphs, replacements,
location=None):
Statement.__init__(self, location)
@@ -1009,6 +1233,14 @@ class ReverseChainSingleSubstStatement(Statement):
class SingleSubstStatement(Statement):
+ """A single substitution statement.
+
+ Note the unusual argument order: ``prefix`` and suffix come `after`
+ the replacement ``glyphs``. ``prefix``, ``suffix``, ``glyphs`` and
+ ``replace`` should be lists of `glyph-containing objects`_. ``glyphs`` and
+ ``replace`` should be one-item lists.
+ """
+
def __init__(self, glyphs, replace, prefix, suffix, forceChain,
location=None):
Statement.__init__(self, location)
@@ -1018,6 +1250,7 @@ class SingleSubstStatement(Statement):
self.replacements = replace
def build(self, builder):
+ """Calls the builder object's ``add_single_subst`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
originals = self.glyphs[0].glyphSet()
@@ -1043,11 +1276,13 @@ class SingleSubstStatement(Statement):
class ScriptStatement(Statement):
+ """A ``script`` statement."""
def __init__(self, script, location=None):
Statement.__init__(self, location)
- self.script = script
+ self.script = script #: the script code
def build(self, builder):
+ """Calls the builder's ``set_script`` callback."""
builder.set_script(self.location, self.script)
def asFea(self, indent=""):
@@ -1055,12 +1290,19 @@ class ScriptStatement(Statement):
class SinglePosStatement(Statement):
+ """A single position statement. ``prefix`` and ``suffix`` should be
+ lists of `glyph-containing objects`_.
+
+ ``pos`` should be a one-element list containing a (`glyph-containing object`_,
+ :class:`ValueRecord`) tuple."""
+
def __init__(self, pos, prefix, suffix, forceChain, location=None):
Statement.__init__(self, location)
self.pos, self.prefix, self.suffix = pos, prefix, suffix
self.forceChain = forceChain
def build(self, builder):
+ """Calls the builder object's ``add_single_pos`` callback."""
prefix = [p.glyphSet() for p in self.prefix]
suffix = [s.glyphSet() for s in self.suffix]
pos = [(g.glyphSet(), value) for g, value in self.pos]
@@ -1084,10 +1326,12 @@ class SinglePosStatement(Statement):
class SubtableStatement(Statement):
+ """Represents a subtable break."""
def __init__(self, location=None):
Statement.__init__(self, location)
def build(self, builder):
+ """Calls the builder objects's ``add_subtable_break`` callback."""
builder.add_subtable_break(self.location)
def asFea(self, indent=""):
@@ -1095,6 +1339,7 @@ class SubtableStatement(Statement):
class ValueRecord(Expression):
+ """Represents a value record."""
def __init__(self, xPlacement=None, yPlacement=None,
xAdvance=None, yAdvance=None,
xPlaDevice=None, yPlaDevice=None,
@@ -1177,10 +1422,11 @@ class ValueRecord(Expression):
class ValueRecordDefinition(Statement):
+ """Represents a named value record definition."""
def __init__(self, name, value, location=None):
Statement.__init__(self, location)
- self.name = name
- self.value = value
+ self.name = name #: Value record name as string
+ self.value = value #: :class:`ValueRecord` object
def asFea(self, indent=""):
return "valueRecordDef {} {};".format(self.value.asFea(), self.name)
@@ -1196,16 +1442,18 @@ def simplify_name_attributes(pid, eid, lid):
class NameRecord(Statement):
+ """Represents a name record. (`Section 9.e. <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_)"""
def __init__(self, nameID, platformID, platEncID, langID, string,
location=None):
Statement.__init__(self, location)
- self.nameID = nameID
- self.platformID = platformID
- self.platEncID = platEncID
- self.langID = langID
- self.string = string
+ self.nameID = nameID #: Name ID as integer (e.g. 9 for designer's name)
+ self.platformID = platformID #: Platform ID as integer
+ self.platEncID = platEncID #: Platform encoding ID as integer
+ self.langID = langID #: Language ID as integer
+ self.string = string #: Name record value
def build(self, builder):
+ """Calls the builder object's ``add_name_record`` callback."""
builder.add_name_record(
self.location, self.nameID, self.platformID,
self.platEncID, self.langID, self.string)
@@ -1235,7 +1483,10 @@ class NameRecord(Statement):
class FeatureNameStatement(NameRecord):
+ """Represents a ``sizemenuname`` or ``name`` statement."""
+
def build(self, builder):
+ """Calls the builder object's ``add_featureName`` callback."""
NameRecord.build(self, builder)
builder.add_featureName(self.nameID)
@@ -1251,6 +1502,7 @@ class FeatureNameStatement(NameRecord):
class SizeParameters(Statement):
+ """A ``parameters`` statement."""
def __init__(self, DesignSize, SubfamilyID, RangeStart, RangeEnd,
location=None):
Statement.__init__(self, location)
@@ -1260,6 +1512,7 @@ class SizeParameters(Statement):
self.RangeEnd = RangeEnd
def build(self, builder):
+ """Calls the builder object's ``set_size_parameters`` callback."""
builder.set_size_parameters(self.location, self.DesignSize,
self.SubfamilyID, self.RangeStart, self.RangeEnd)
@@ -1271,6 +1524,7 @@ class SizeParameters(Statement):
class CVParametersNameStatement(NameRecord):
+ """Represent a name statement inside a ``cvParameters`` block."""
def __init__(self, nameID, platformID, platEncID, langID, string,
block_name, location=None):
NameRecord.__init__(self, nameID, platformID, platEncID, langID,
@@ -1278,6 +1532,7 @@ class CVParametersNameStatement(NameRecord):
self.block_name = block_name
def build(self, builder):
+ """Calls the builder object's ``add_cv_parameter`` callback."""
item = ""
if self.block_name == "ParamUILabelNameID":
item = "_{}".format(builder.cv_num_named_params_.get(self.nameID, 0))
@@ -1306,6 +1561,7 @@ class CharacterStatement(Statement):
self.tag = tag
def build(self, builder):
+ """Calls the builder object's ``add_cv_character`` callback."""
builder.add_cv_character(self.character, self.tag)
def asFea(self, indent=""):
@@ -1313,13 +1569,16 @@ class CharacterStatement(Statement):
class BaseAxis(Statement):
+ """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList``
+ pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair."""
def __init__(self, bases, scripts, vertical, location=None):
Statement.__init__(self, location)
- self.bases = bases
- self.scripts = scripts
- self.vertical = vertical
+ self.bases = bases #: A list of baseline tag names as strings
+ self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate)
+ self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False
def build(self, builder):
+ """Calls the builder object's ``set_base_axis`` callback."""
builder.set_base_axis(self.bases, self.scripts, self.vertical)
def asFea(self, indent=""):
@@ -1330,12 +1589,16 @@ class BaseAxis(Statement):
class OS2Field(Statement):
+ """An entry in the ``OS/2`` table. Most ``values`` should be numbers or
+ strings, apart from when the key is ``UnicodeRange``, ``CodePageRange``
+ or ``Panose``, in which case it should be an array of integers."""
def __init__(self, key, value, location=None):
Statement.__init__(self, location)
self.key = key
self.value = value
def build(self, builder):
+ """Calls the builder object's ``add_os2_field`` callback."""
builder.add_os2_field(self.key, self.value)
def asFea(self, indent=""):
@@ -1355,12 +1618,14 @@ class OS2Field(Statement):
class HheaField(Statement):
+ """An entry in the ``hhea`` table."""
def __init__(self, key, value, location=None):
Statement.__init__(self, location)
self.key = key
self.value = value
def build(self, builder):
+ """Calls the builder object's ``add_hhea_field`` callback."""
builder.add_hhea_field(self.key, self.value)
def asFea(self, indent=""):
@@ -1370,12 +1635,14 @@ class HheaField(Statement):
class VheaField(Statement):
+ """An entry in the ``vhea`` table."""
def __init__(self, key, value, location=None):
Statement.__init__(self, location)
self.key = key
self.value = value
def build(self, builder):
+ """Calls the builder object's ``add_vhea_field`` callback."""
builder.add_vhea_field(self.key, self.value)
def asFea(self, indent=""):
diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py
index 52b23f1b..c1dc920e 100644
--- a/Lib/fontTools/feaLib/builder.py
+++ b/Lib/fontTools/feaLib/builder.py
@@ -17,15 +17,38 @@ log = logging.getLogger(__name__)
def addOpenTypeFeatures(font, featurefile, tables=None):
+ """Add features from a file to a font. Note that this replaces any features
+ currently present.
+
+ Args:
+ font (feaLib.ttLib.TTFont): The font object.
+ featurefile: Either a path or file object (in which case we
+ parse it into an AST), or a pre-parsed AST instance.
+ tables: If passed, restrict the set of affected tables to those in the
+ list.
+
+ """
builder = Builder(font, featurefile)
builder.build(tables=tables)
def addOpenTypeFeaturesFromString(font, features, filename=None, tables=None):
+ """Add features from a string to a font. Note that this replaces any
+ features currently present.
+
+ Args:
+ font (feaLib.ttLib.TTFont): The font object.
+ features: A string containing feature code.
+ filename: The directory containing ``filename`` is used as the root of
+ relative ``include()`` paths; if ``None`` is provided, the current
+ directory is assumed.
+ tables: If passed, restrict the set of affected tables to those in the
+ list.
+
+ """
+
featurefile = UnicodeIO(tounicode(features))
if filename:
- # the directory containing 'filename' is used as the root of relative
- # include paths; if None is provided, the current directory is assumed
featurefile.name = filename
addOpenTypeFeatures(font, featurefile, tables=tables)
@@ -203,9 +226,12 @@ class Builder(object):
raise FeatureLibError("Feature %s has not been defined" % name,
location)
for script, lang, feature, lookups in feature:
- for lookup in lookups:
- for glyph, alts in lookup.getAlternateGlyphs().items():
- alternates.setdefault(glyph, set()).update(alts)
+ for lookuplist in lookups:
+ if not isinstance(lookuplist, list):
+ lookuplist = [lookuplist]
+ for lookup in lookuplist:
+ for glyph, alts in lookup.getAlternateGlyphs().items():
+ alternates.setdefault(glyph, set()).update(alts)
single = {glyph: list(repl)[0] for glyph, repl in alternates.items()
if len(repl) == 1}
# TODO: Figure out the glyph alternate ordering used by makeotf.
@@ -797,9 +823,10 @@ class Builder(object):
If an input name is None, it gets mapped to a None LookupBuilder.
"""
lookup_builders = []
- for lookup in lookups:
- if lookup is not None:
- lookup_builders.append(self.named_lookups_.get(lookup.name))
+ for lookuplist in lookups:
+ if lookuplist is not None:
+ lookup_builders.append([self.named_lookups_.get(l.name)
+ for l in lookuplist])
else:
lookup_builders.append(None)
return lookup_builders
@@ -851,7 +878,7 @@ class Builder(object):
self.cv_parameters_.add(tag)
def add_to_cv_num_named_params(self, tag):
- """Adds new items to self.cv_num_named_params_
+ """Adds new items to ``self.cv_num_named_params_``
or increments the count of existing items."""
if tag in self.cv_num_named_params_:
self.cv_num_named_params_[tag] += 1
@@ -1259,18 +1286,23 @@ class ChainContextPosBuilder(LookupBuilder):
self.setLookAheadCoverage_(suffix, st)
self.setInputCoverage_(glyphs, st)
- st.PosCount = len([l for l in lookups if l is not None])
+ st.PosCount = 0
st.PosLookupRecord = []
- for sequenceIndex, l in enumerate(lookups):
- if l is not None:
- if l.lookup_index is None:
- raise FeatureLibError('Missing index of the specified '
- 'lookup, might be a substitution lookup',
- self.location)
- rec = otTables.PosLookupRecord()
- rec.SequenceIndex = sequenceIndex
- rec.LookupListIndex = l.lookup_index
- st.PosLookupRecord.append(rec)
+ for sequenceIndex, lookupList in enumerate(lookups):
+ if lookupList is not None:
+ if not isinstance(lookupList, list):
+ # Can happen with synthesised lookups
+ lookupList = [ lookupList ]
+ for l in lookupList:
+ st.PosCount += 1
+ if l.lookup_index is None:
+ raise FeatureLibError('Missing index of the specified '
+ 'lookup, might be a substitution lookup',
+ self.location)
+ rec = otTables.PosLookupRecord()
+ rec.SequenceIndex = sequenceIndex
+ rec.LookupListIndex = l.lookup_index
+ st.PosLookupRecord.append(rec)
return self.buildLookup_(subtables)
def find_chainable_single_pos(self, lookups, glyphs, value):
@@ -1310,30 +1342,38 @@ class ChainContextSubstBuilder(LookupBuilder):
self.setLookAheadCoverage_(suffix, st)
self.setInputCoverage_(input, st)
- st.SubstCount = len([l for l in lookups if l is not None])
+ st.SubstCount = 0
st.SubstLookupRecord = []
- for sequenceIndex, l in enumerate(lookups):
- if l is not None:
- if l.lookup_index is None:
- raise FeatureLibError('Missing index of the specified '
- 'lookup, might be a positioning lookup',
- self.location)
- rec = otTables.SubstLookupRecord()
- rec.SequenceIndex = sequenceIndex
- rec.LookupListIndex = l.lookup_index
- st.SubstLookupRecord.append(rec)
+ for sequenceIndex, lookupList in enumerate(lookups):
+ if lookupList is not None:
+ if not isinstance(lookupList, list):
+ # Can happen with synthesised lookups
+ lookupList = [ lookupList ]
+ for l in lookupList:
+ st.SubstCount += 1
+ if l.lookup_index is None:
+ raise FeatureLibError('Missing index of the specified '
+ 'lookup, might be a positioning lookup',
+ self.location)
+ rec = otTables.SubstLookupRecord()
+ rec.SequenceIndex = sequenceIndex
+ rec.LookupListIndex = l.lookup_index
+ st.SubstLookupRecord.append(rec)
return self.buildLookup_(subtables)
def getAlternateGlyphs(self):
result = {}
- for (_, _, _, lookups) in self.substitutions:
- if lookups == self.SUBTABLE_BREAK_:
+ for (_, _, _, lookuplist) in self.substitutions:
+ if lookuplist == self.SUBTABLE_BREAK_:
continue
- for lookup in lookups:
- if lookup is not None:
- alts = lookup.getAlternateGlyphs()
- for glyph, replacements in alts.items():
- result.setdefault(glyph, set()).update(replacements)
+ for lookups in lookuplist:
+ if not isinstance(lookups, list):
+ lookups = [lookups]
+ for lookup in lookups:
+ if lookup is not None:
+ alts = lookup.getAlternateGlyphs()
+ for glyph, replacements in alts.items():
+ result.setdefault(glyph, set()).update(replacements)
return result
def find_chainable_single_subst(self, glyphs):
diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py
index a3eaf626..3a63c6e0 100644
--- a/Lib/fontTools/feaLib/parser.py
+++ b/Lib/fontTools/feaLib/parser.py
@@ -12,6 +12,27 @@ log = logging.getLogger(__name__)
class Parser(object):
+ """Initializes a Parser object.
+
+ Example:
+
+ .. code:: python
+
+ from fontTools.feaLib.parser import Parser
+ parser = Parser(file, font.getReverseGlyphMap())
+ parsetree = parser.parse()
+
+ Note: the ``glyphNames`` iterable serves a double role to help distinguish
+ glyph names from ranges in the presence of hyphens and to ensure that glyph
+ names referenced in a feature file are actually part of a font's glyph set.
+ If the iterable is left empty, no glyph name in glyph set checking takes
+ place, and all glyph tokens containing hyphens are treated as literal glyph
+ names, not as ranges. (Adding a space around the hyphen can, in any case,
+ help to disambiguate ranges from glyph names containing hyphens.)
+
+ By default, the parser will follow ``include()`` statements in the feature
+ file. To turn this off, pass ``followIncludes=False``.
+ """
extensions = {}
ast = ast
SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)}
@@ -19,14 +40,7 @@ class Parser(object):
def __init__(self, featurefile, glyphNames=(), followIncludes=True,
**kwargs):
- """Initializes a Parser object.
- Note: the `glyphNames` iterable serves a double role to help distinguish
- glyph names from ranges in the presence of hyphens and to ensure that glyph
- names referenced in a feature file are actually part of a font's glyph set.
- If the iterable is left empty, no glyph name in glyph set checking takes
- place.
- """
if "glyphMap" in kwargs:
from fontTools.misc.loggingTools import deprecateArgument
deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
@@ -56,6 +70,9 @@ class Parser(object):
self.advance_lexer_(comments=True)
def parse(self):
+ """Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile`
+ object representing the root of the abstract syntax tree containing the
+ parsed contents of the file."""
statements = self.doc_.statements
while self.next_token_type_ is not None or self.cur_comments_:
self.advance_lexer_(comments=True)
@@ -96,16 +113,18 @@ class Parser(object):
return self.doc_
def parse_anchor_(self):
+ # Parses an anchor in any of the four formats given in the feature
+ # file specification (2.e.vii).
self.expect_symbol_("<")
self.expect_keyword_("anchor")
location = self.cur_token_location_
- if self.next_token_ == "NULL":
+ if self.next_token_ == "NULL": # Format D
self.expect_keyword_("NULL")
self.expect_symbol_(">")
return None
- if self.next_token_type_ == Lexer.NAME:
+ if self.next_token_type_ == Lexer.NAME: # Format E
name = self.expect_name_()
anchordef = self.anchors_.resolve(name)
if anchordef is None:
@@ -122,11 +141,11 @@ class Parser(object):
x, y = self.expect_number_(), self.expect_number_()
contourpoint = None
- if self.next_token_ == "contourpoint":
+ if self.next_token_ == "contourpoint": # Format B
self.expect_keyword_("contourpoint")
contourpoint = self.expect_number_()
- if self.next_token_ == "<":
+ if self.next_token_ == "<": # Format C
xDeviceTable = self.parse_device_()
yDeviceTable = self.parse_device_()
else:
@@ -140,7 +159,7 @@ class Parser(object):
location=location)
def parse_anchor_marks_(self):
- """Parses a sequence of [<anchor> mark @MARKCLASS]*."""
+ # Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
anchorMarks = [] # [(self.ast.Anchor, markClassName)*]
while self.next_token_ == "<":
anchor = self.parse_anchor_()
@@ -152,6 +171,7 @@ class Parser(object):
return anchorMarks
def parse_anchordef_(self):
+ # Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
assert self.is_cur_keyword_("anchorDef")
location = self.cur_token_location_
x, y = self.expect_number_(), self.expect_number_()
@@ -168,6 +188,7 @@ class Parser(object):
return anchordef
def parse_anonymous_(self):
+ # Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
assert self.is_cur_keyword_(("anon", "anonymous"))
tag = self.expect_tag_()
_, content, location = self.lexer_.scan_anonymous_block(tag)
@@ -179,6 +200,7 @@ class Parser(object):
return self.ast.AnonymousBlock(tag, content, location=location)
def parse_attach_(self):
+ # Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
assert self.is_cur_keyword_("Attach")
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
@@ -190,12 +212,13 @@ class Parser(object):
location=location)
def parse_enumerate_(self, vertical):
+ # Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
assert self.cur_token_ in {"enumerate", "enum"}
self.advance_lexer_()
return self.parse_position_(enumerated=True, vertical=vertical)
def parse_GlyphClassDef_(self):
- """Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'"""
+ # Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
assert self.is_cur_keyword_("GlyphClassDef")
location = self.cur_token_location_
if self.next_token_ != ",":
@@ -223,7 +246,7 @@ class Parser(object):
location=location)
def parse_glyphclass_definition_(self):
- """Parses glyph class definitions such as '@UPPERCASE = [A-Z];'"""
+ # Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
location, name = self.cur_token_location_, self.cur_token_
self.expect_symbol_("=")
glyphs = self.parse_glyphclass_(accept_glyphname=False)
@@ -273,6 +296,8 @@ class Parser(object):
location)
def parse_glyphclass_(self, accept_glyphname):
+ # Parses a glyph class, either named or anonymous, or (if
+ # ``bool(accept_glyphname)``) a glyph name.
if (accept_glyphname and
self.next_token_type_ in (Lexer.NAME, Lexer.CID)):
glyph = self.expect_glyph_()
@@ -362,6 +387,7 @@ class Parser(object):
return glyphs
def parse_class_name_(self):
+ # Parses named class - either a glyph class or mark class.
name = self.expect_class_name_()
gc = self.glyphclasses_.resolve(name)
if gc is None:
@@ -376,6 +402,11 @@ class Parser(object):
gc, location=self.cur_token_location_)
def parse_glyph_pattern_(self, vertical):
+ # Parses a glyph pattern, including lookups and context, e.g.::
+ #
+ # a b
+ # a b c' d e
+ # a b c' lookup ChangeC d e
prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
hasMarks = False
while self.next_token_ not in {"by", "from", ";", ","}:
@@ -404,8 +435,10 @@ class Parser(object):
else:
values.append(None)
- lookup = None
- if self.next_token_ == "lookup":
+ lookuplist = None
+ while self.next_token_ == "lookup":
+ if lookuplist is None:
+ lookuplist = []
self.expect_keyword_("lookup")
if not marked:
raise FeatureLibError(
@@ -417,8 +450,9 @@ class Parser(object):
raise FeatureLibError(
'Unknown lookup "%s"' % lookup_name,
self.cur_token_location_)
+ lookuplist.append(lookup)
if marked:
- lookups.append(lookup)
+ lookups.append(lookuplist)
if not glyphs and not suffix: # eg., "sub f f i by"
assert lookups == []
@@ -446,6 +480,7 @@ class Parser(object):
return chainContext, hasLookups
def parse_ignore_(self):
+ # Parses an ignore sub/pos rule.
assert self.is_cur_keyword_("ignore")
location = self.cur_token_location_
self.advance_lexer_()
@@ -514,6 +549,8 @@ class Parser(object):
location=location)
def parse_lookup_(self, vertical):
+ # Parses a ``lookup`` - either a lookup block, or a lookup reference
+ # inside a feature.
assert self.is_cur_keyword_("lookup")
location, name = self.cur_token_location_, self.expect_name_()
@@ -537,6 +574,8 @@ class Parser(object):
return block
def parse_lookupflag_(self):
+ # Parses a ``lookupflag`` statement, either specified by number or
+ # in words.
assert self.is_cur_keyword_("lookupflag")
location = self.cur_token_location_
@@ -850,6 +889,8 @@ class Parser(object):
return self.ast.SubtableStatement(location=location)
def parse_size_parameters_(self):
+ # Parses a ``parameters`` statement used in ``size`` features. See
+ # `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
assert self.is_cur_keyword_("parameters")
location = self.cur_token_location_
DesignSize = self.expect_decipoint_()
@@ -1003,6 +1044,7 @@ class Parser(object):
self.cur_token_location_)
def parse_name_(self):
+ """Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
platEncID = None
langID = None
if self.next_token_type_ in Lexer.NUMBERS:
@@ -1130,6 +1172,7 @@ class Parser(object):
continue
def parse_base_tag_list_(self):
+ # Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
assert self.cur_token_ in ("HorizAxis.BaseTagList",
"VertAxis.BaseTagList"), self.cur_token_
bases = []
@@ -1229,6 +1272,7 @@ class Parser(object):
vertical=vertical, location=location)
def parse_valuerecord_definition_(self, vertical):
+ # Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
assert self.is_cur_keyword_("valueRecordDef")
location = self.cur_token_location_
value = self.parse_valuerecord_(vertical)
@@ -1283,6 +1327,8 @@ class Parser(object):
location=location)
def parse_featureNames_(self, tag):
+ """Parses a ``featureNames`` statement found in stylistic set features.
+ See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_."""
assert self.cur_token_ == "featureNames", self.cur_token_
block = self.ast.NestedBlock(tag, self.cur_token_,
location=self.cur_token_location_)
@@ -1313,6 +1359,8 @@ class Parser(object):
return block
def parse_cvParameters_(self, tag):
+ # Parses a ``cvParameters`` block found in Character Variant features.
+ # See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
assert self.cur_token_ == "cvParameters", self.cur_token_
block = self.ast.NestedBlock(tag, self.cur_token_,
location=self.cur_token_location_)
@@ -1388,6 +1436,8 @@ class Parser(object):
return self.ast.CharacterStatement(character, tag, location=location)
def parse_FontRevision_(self):
+ # Parses a ``FontRevision`` statement found in the head table. See
+ # `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
assert self.cur_token_ == "FontRevision", self.cur_token_
location, version = self.cur_token_location_, self.expect_float_()
self.expect_symbol_(";")
diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py
index d4b94051..f3fe92a8 100644
--- a/Lib/fontTools/fontBuilder.py
+++ b/Lib/fontTools/fontBuilder.py
@@ -803,6 +803,15 @@ class FontBuilder(object):
nameTable=self.font.get("name")
)
+ def setupStat(self, axes, locations=None, elidedFallbackName=2):
+ """Build a new 'STAT' table.
+
+ See `fontTools.otlLib.builder.buildStatTable` for details about
+ the arguments.
+ """
+ from .otlLib.builder import buildStatTable
+ buildStatTable(self.font, axes, locations, elidedFallbackName)
+
def buildCmapSubTable(cmapping, format, platformID, platEncID):
subTable = cmap_classes[format](format)
diff --git a/Lib/fontTools/help.py b/Lib/fontTools/help.py
new file mode 100644
index 00000000..ff8048d5
--- /dev/null
+++ b/Lib/fontTools/help.py
@@ -0,0 +1,34 @@
+import pkgutil
+import sys
+import fontTools
+import importlib
+import os
+from pathlib import Path
+
+
+def main():
+ """Show this help"""
+ path = fontTools.__path__
+ descriptions = {}
+ for pkg in sorted(
+ mod.name
+ for mod in pkgutil.walk_packages([fontTools.__path__[0]], prefix="fontTools.")
+ ):
+ try:
+ imports = __import__(pkg, globals(), locals(), ["main"])
+ except ImportError as e:
+ continue
+ try:
+ description = imports.main.__doc__
+ if description:
+ pkg = pkg.replace("fontTools.", "").replace(".__main__", "")
+ descriptions[pkg] = description
+ except AttributeError as e:
+ pass
+ for pkg, description in descriptions.items():
+ print("fonttools %-12s %s" % (pkg, description), file=sys.stderr)
+
+
+if __name__ == "__main__":
+ print("fonttools v%s\n" % fontTools.__version__, file=sys.stderr)
+ main()
diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py
index 9ef31f8e..890234df 100644
--- a/Lib/fontTools/merge.py
+++ b/Lib/fontTools/merge.py
@@ -2,9 +2,6 @@
#
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
-"""Font merger.
-"""
-
from fontTools.misc.py23 import *
from fontTools.misc.timeTools import timestampNow
from fontTools import ttLib, cffLib
@@ -294,11 +291,18 @@ ttLib.getTableClass('OS/2').mergeMap = {
'sTypoLineGap': max,
'usWinAscent': max,
'usWinDescent': max,
- # Version 2,3,4
+ # Version 1
'ulCodePageRange1': onlyExisting(bitwise_or),
'ulCodePageRange2': onlyExisting(bitwise_or),
- 'usMaxContex': onlyExisting(max),
- # TODO version 5
+ # Version 2, 3, 4
+ 'sxHeight': onlyExisting(max),
+ 'sCapHeight': onlyExisting(max),
+ 'usDefaultChar': onlyExisting(first),
+ 'usBreakChar': onlyExisting(first),
+ 'usMaxContext': onlyExisting(max),
+ # version 5
+ 'usLowerOpticalPointSize': onlyExisting(min),
+ 'usUpperOpticalPointSize': onlyExisting(max),
}
@_add_method(ttLib.getTableClass('OS/2'))
@@ -944,6 +948,34 @@ class _NonhashableDict(object):
del self.d[id(k)]
class Merger(object):
+ """Font merger.
+
+ This class merges multiple files into a single OpenType font, taking into
+ account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and
+ cross-font metrics (e.g. ``hhea.ascent`` is set to the maximum value across
+ all the fonts).
+
+ If multiple glyphs map to the same Unicode value, and the glyphs are considered
+ sufficiently different (that is, they differ in any of paths, widths, or
+ height), then subsequent glyphs are renamed and a lookup in the ``locl``
+ feature will be created to disambiguate them. For example, if the arguments
+ are an Arabic font and a Latin font and both contain a set of parentheses,
+ the Latin glyphs will be renamed to ``parenleft#1`` and ``parenright#1``,
+ and a lookup will be inserted into the to ``locl`` feature (creating it if
+ necessary) under the ``latn`` script to substitute ``parenleft`` with
+ ``parenleft#1`` etc.
+
+ Restrictions:
+
+ - All fonts must currently have TrueType outlines (``glyf`` table).
+ Merging fonts with CFF outlines is not supported.
+ - All fonts must have the same units per em.
+ - If duplicate glyph disambiguation takes place as described above then the
+ fonts must have a ``GSUB`` table.
+
+ Attributes:
+ options: Currently unused.
+ """
def __init__(self, options=None):
@@ -953,7 +985,15 @@ class Merger(object):
self.options = options
def merge(self, fontfiles):
+ """Merges fonts together.
+
+ Args:
+ fontfiles: A list of file names to be merged
+ Returns:
+ A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on
+ this to write it out to an OTF file.
+ """
mega = ttLib.TTFont()
#
@@ -974,7 +1014,7 @@ class Merger(object):
self._preMerge(font)
self.fonts = fonts
- self.duplicateGlyphsPerFont = [{} for f in fonts]
+ self.duplicateGlyphsPerFont = [{} for _ in fonts]
allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
allTags.remove('GlyphOrder')
@@ -1136,6 +1176,7 @@ __all__ = [
@timer("make one with everything (TOTAL TIME)")
def main(args=None):
+ """Merge multiple fonts into one"""
from fontTools import configLogger
if args is None:
diff --git a/Lib/fontTools/misc/loggingTools.py b/Lib/fontTools/misc/loggingTools.py
index e08aec98..d9c1dbb3 100644
--- a/Lib/fontTools/misc/loggingTools.py
+++ b/Lib/fontTools/misc/loggingTools.py
@@ -409,13 +409,13 @@ class ChannelsFilter(logging.Filter):
def __init__(self, *names):
self.names = names
self.num = len(names)
- self.lenghts = {n: len(n) for n in names}
+ self.lengths = {n: len(n) for n in names}
def filter(self, record):
if self.num == 0:
return True
for name in self.names:
- nlen = self.lenghts[name]
+ nlen = self.lengths[name]
if name == record.name:
return True
elif (record.name.find(name, 0, nlen) == 0
diff --git a/Lib/fontTools/misc/psCharStrings.py b/Lib/fontTools/misc/psCharStrings.py
index b894653d..5f1427d0 100644
--- a/Lib/fontTools/misc/psCharStrings.py
+++ b/Lib/fontTools/misc/psCharStrings.py
@@ -1150,10 +1150,7 @@ class T1CharString(T2CharString):
operators, opcodes = buildOperatorDict(t1Operators)
def __init__(self, bytecode=None, program=None, subrs=None):
- if program is None:
- program = []
- self.bytecode = bytecode
- self.program = program
+ super().__init__(bytecode, program)
self.subrs = subrs
def getIntEncoder(self):
diff --git a/Lib/fontTools/misc/testTools.py b/Lib/fontTools/misc/testTools.py
index 59055062..be9bc851 100644
--- a/Lib/fontTools/misc/testTools.py
+++ b/Lib/fontTools/misc/testTools.py
@@ -68,6 +68,9 @@ class FakeFont:
def getReverseGlyphMap(self):
return self.reverseGlyphOrderDict_
+ def getGlyphNames(self):
+ return sorted(self.getGlyphOrder())
+
class TestXMLReader_(object):
def __init__(self):
diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py
index f0f0a133..4176fb25 100644
--- a/Lib/fontTools/mtiLib/__init__.py
+++ b/Lib/fontTools/mtiLib/__init__.py
@@ -1151,6 +1151,7 @@ def build(f, font, tableTag=None):
def main(args=None, font=None):
+ """Convert a FontDame OTL file to TTX XML"""
import sys
from fontTools import configLogger
from fontTools.misc.testTools import MockFont
@@ -1163,17 +1164,31 @@ def main(args=None, font=None):
# comment this out to enable debug messages from mtiLib's logger
# log.setLevel(logging.DEBUG)
+ import argparse
+ parser = argparse.ArgumentParser(
+ "fonttools mtiLib",
+ description=main.__doc__,
+ )
+
+ parser.add_argument('--font', '-f', metavar='FILE', dest="font",
+ help="Input TTF files (used for glyph classes and sorting coverage tables)")
+ parser.add_argument('--table', '-t', metavar='TABLE', dest="tableTag",
+ help="Table to fill (sniffed from input file if not provided)")
+ parser.add_argument('inputs', metavar='FILE', type=str, nargs='+',
+ help="Input FontDame .txt files")
+
+ args = parser.parse_args(args)
+
if font is None:
- font = MockFont()
+ if args.font:
+ font = ttLib.TTFont(args.font)
+ else:
+ font = MockFont()
- tableTag = None
- if args[0].startswith('-t'):
- tableTag = args[0][2:]
- del args[0]
- for f in args:
+ for f in args.inputs:
log.debug("Processing %s", f)
with open(f, 'rt', encoding="utf-8") as f:
- table = build(f, font, tableTag=tableTag)
+ table = build(f, font, tableTag=args.tableTag)
blob = table.compile(font) # Make sure it compiles
decompiled = table.__class__()
decompiled.decompile(blob, font) # Make sure it decompiles!
diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py
index dd0aabe7..4d9d2bc0 100644
--- a/Lib/fontTools/otlLib/builder.py
+++ b/Lib/fontTools/otlLib/builder.py
@@ -1,4 +1,5 @@
from collections import namedtuple
+from fontTools.misc.fixedTools import fixedToFloat
from fontTools import ttLib
from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import ValueRecord, valueRecordFormatDict
@@ -657,3 +658,193 @@ class ClassDefBuilder(object):
classDef = ot.ClassDef()
classDef.classDefs = glyphClasses
return classDef
+
+
+AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16)
+AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16)
+
+
+def buildStatTable(ttFont, axes, locations=None, elidedFallbackName=2):
+ """Add a 'STAT' table to 'ttFont'.
+
+ 'axes' is a list of dictionaries describing axes and their
+ values.
+
+ Example:
+
+ axes = [
+ dict(
+ tag="wght",
+ name="Weight",
+ ordering=0, # optional
+ values=[
+ dict(value=100, name='Thin'),
+ dict(value=300, name='Light'),
+ dict(value=400, name='Regular', flags=0x2),
+ dict(value=900, name='Black'),
+ ],
+ )
+ ]
+
+ Each axis dict must have 'tag' and 'name' items. 'tag' maps
+ to the 'AxisTag' field. 'name' can be a name ID (int), a string,
+ or a dictionary containing multilingual names (see the
+ addMultilingualName() name table method), and will translate to
+ the AxisNameID field.
+
+ An axis dict may contain an 'ordering' item that maps to the
+ AxisOrdering field. If omitted, the order of the axes list is
+ used to calculate AxisOrdering fields.
+
+ The axis dict may contain a 'values' item, which is a list of
+ dictionaries describing AxisValue records belonging to this axis.
+
+ Each value dict must have a 'name' item, which can be a name ID
+ (int), a string, or a dictionary containing multilingual names,
+ like the axis name. It translates to the ValueNameID field.
+
+ Optionally the value dict can contain a 'flags' item. It maps to
+ the AxisValue Flags field, and will be 0 when omitted.
+
+ The format of the AxisValue is determined by the remaining contents
+ of the value dictionary:
+
+ If the value dict contains a 'value' item, an AxisValue record
+ Format 1 is created. If in addition to the 'value' item it contains
+ a 'linkedValue' item, an AxisValue record Format 3 is built.
+
+ If the value dict contains a 'nominalValue' item, an AxisValue
+ record Format 2 is built. Optionally it may contain 'rangeMinValue'
+ and 'rangeMaxValue' items. These map to -Infinity and +Infinity
+ respectively if omitted.
+
+ You cannot specify Format 4 AxisValue tables this way, as they are
+ not tied to a single axis, and specify a name for a location that
+ is defined by multiple axes values. Instead, you need to supply the
+ 'locations' argument.
+
+ The optional 'locations' argument specifies AxisValue Format 4
+ tables. It should be a list of dicts, where each dict has a 'name'
+ item, which works just like the value dicts above, an optional
+ 'flags' item (defaulting to 0x0), and a 'location' dict. A
+ location dict key is an axis tag, and the associated value is the
+ location on the specified axis. They map to the AxisIndex and Value
+ fields of the AxisValueRecord.
+
+ Example:
+
+ locations = [
+ dict(name='Regular ABCD', location=dict(wght=300, ABCD=100)),
+ dict(name='Bold ABCD XYZ', location=dict(wght=600, ABCD=200)),
+ ]
+
+ The optional 'elidedFallbackName' argument can be a name ID (int),
+ a string, or a dictionary containing multilingual names. It
+ translates to the ElidedFallbackNameID field.
+
+ The 'ttFont' argument must be a TTFont instance that already has a
+ 'name' table. If a 'STAT' table already exists, it will be
+ overwritten by the newly created one.
+ """
+ ttFont["STAT"] = ttLib.newTable("STAT")
+ statTable = ttFont["STAT"].table = ot.STAT()
+ nameTable = ttFont["name"]
+ statTable.ElidedFallbackNameID = _addName(nameTable, elidedFallbackName)
+
+ # 'locations' contains data for AxisValue Format 4
+ axisRecords, axisValues = _buildAxisRecords(axes, nameTable)
+ if not locations:
+ statTable.Version = 0x00010001
+ else:
+ # We'll be adding Format 4 AxisValue records, which
+ # requires a higher table version
+ statTable.Version = 0x00010002
+ multiAxisValues = _buildAxisValuesFormat4(locations, axes, nameTable)
+ axisValues = multiAxisValues + axisValues
+
+ # Store AxisRecords
+ axisRecordArray = ot.AxisRecordArray()
+ axisRecordArray.Axis = axisRecords
+ # XXX these should not be hard-coded but computed automatically
+ statTable.DesignAxisRecordSize = 8
+ statTable.DesignAxisRecord = axisRecordArray
+ statTable.DesignAxisCount = len(axisRecords)
+
+ if axisValues:
+ # Store AxisValueRecords
+ axisValueArray = ot.AxisValueArray()
+ axisValueArray.AxisValue = axisValues
+ statTable.AxisValueArray = axisValueArray
+ statTable.AxisValueCount = len(axisValues)
+
+
+def _buildAxisRecords(axes, nameTable):
+ axisRecords = []
+ axisValues = []
+ for axisRecordIndex, axisDict in enumerate(axes):
+ axis = ot.AxisRecord()
+ axis.AxisTag = axisDict["tag"]
+ axis.AxisNameID = _addName(nameTable, axisDict["name"])
+ axis.AxisOrdering = axisDict.get("ordering", axisRecordIndex)
+ axisRecords.append(axis)
+
+ for axisVal in axisDict.get("values", ()):
+ axisValRec = ot.AxisValue()
+ axisValRec.AxisIndex = axisRecordIndex
+ axisValRec.Flags = axisVal.get("flags", 0)
+ axisValRec.ValueNameID = _addName(nameTable, axisVal['name'])
+
+ if "value" in axisVal:
+ axisValRec.Value = axisVal["value"]
+ if "linkedValue" in axisVal:
+ axisValRec.Format = 3
+ axisValRec.LinkedValue = axisVal["linkedValue"]
+ else:
+ axisValRec.Format = 1
+ elif "nominalValue" in axisVal:
+ axisValRec.Format = 2
+ axisValRec.NominalValue = axisVal["nominalValue"]
+ axisValRec.RangeMinValue = axisVal.get("rangeMinValue", AXIS_VALUE_NEGATIVE_INFINITY)
+ axisValRec.RangeMaxValue = axisVal.get("rangeMaxValue", AXIS_VALUE_POSITIVE_INFINITY)
+ else:
+ raise ValueError("Can't determine format for AxisValue")
+
+ axisValues.append(axisValRec)
+ return axisRecords, axisValues
+
+
+def _buildAxisValuesFormat4(locations, axes, nameTable):
+ axisTagToIndex = {}
+ for axisRecordIndex, axisDict in enumerate(axes):
+ axisTagToIndex[axisDict["tag"]] = axisRecordIndex
+
+ axisValues = []
+ for axisLocationDict in locations:
+ axisValRec = ot.AxisValue()
+ axisValRec.Format = 4
+ axisValRec.ValueNameID = _addName(nameTable, axisLocationDict['name'])
+ axisValRec.Flags = axisLocationDict.get("flags", 0)
+ axisValueRecords = []
+ for tag, value in axisLocationDict["location"].items():
+ avr = ot.AxisValueRecord()
+ avr.AxisIndex = axisTagToIndex[tag]
+ avr.Value = value
+ axisValueRecords.append(avr)
+ axisValueRecords.sort(key=lambda avr: avr.AxisIndex)
+ axisValRec.AxisCount = len(axisValueRecords)
+ axisValRec.AxisValueRecord = axisValueRecords
+ axisValues.append(axisValRec)
+ return axisValues
+
+
+def _addName(nameTable, value):
+ if isinstance(value, int):
+ # Already a nameID
+ return value
+ if isinstance(value, str):
+ names = dict(en=value)
+ elif isinstance(value, dict):
+ names = value
+ else:
+ raise TypeError("value must be int, str or dict")
+ return nameTable.addMultilingualName(names)
diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py
index 0119a92b..7f8aba87 100644
--- a/Lib/fontTools/subset/__init__.py
+++ b/Lib/fontTools/subset/__init__.py
@@ -894,6 +894,8 @@ def __subset_classify_context(self):
self.ClassDef = 'InputClassDef' if Chain else 'ClassDef'
self.ClassDefIndex = 1 if Chain else 0
self.Input = 'Input' if Chain else 'Class'
+ elif Format == 3:
+ self.Input = 'InputCoverage' if Chain else 'Coverage'
if self.Format not in [1, 2, 3]:
return None # Don't shoot the messenger; let it go
@@ -976,6 +978,7 @@ def closure_glyphs(self, s, cur_glyphs):
if not all(x.intersect(s.glyphs) for x in c.RuleData(self)):
return []
r = self
+ input_coverages = getattr(r, c.Input)
chaos = set()
for ll in getattr(r, c.LookupRecord):
if not ll: continue
@@ -987,11 +990,11 @@ def closure_glyphs(self, s, cur_glyphs):
if seqi == 0:
pos_glyphs = frozenset(cur_glyphs)
else:
- pos_glyphs = frozenset(r.InputCoverage[seqi].intersect_glyphs(s.glyphs))
+ pos_glyphs = frozenset(input_coverages[seqi].intersect_glyphs(s.glyphs))
lookup = s.table.LookupList.Lookup[ll.LookupListIndex]
chaos.add(seqi)
if lookup.may_have_non_1to1():
- chaos.update(range(seqi, len(r.InputCoverage)+1))
+ chaos.update(range(seqi, len(input_coverages)+1))
lookup.closure_glyphs(s, cur_glyphs=pos_glyphs)
else:
assert 0, "unknown format: %s" % self.Format
@@ -2778,6 +2781,7 @@ def usage():
@timer("make one with everything (TOTAL TIME)")
def main(args=None):
+ """OpenType font subsetter and optimizer"""
from os.path import splitext
from fontTools import configLogger
diff --git a/Lib/fontTools/subset/__main__.py b/Lib/fontTools/subset/__main__.py
index 3f3d894a..93549d5d 100644
--- a/Lib/fontTools/subset/__main__.py
+++ b/Lib/fontTools/subset/__main__.py
@@ -2,5 +2,6 @@ from fontTools.misc.py23 import *
import sys
from fontTools.subset import main
+
if __name__ == '__main__':
sys.exit(main())
diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py
index 9be149e4..9c45305d 100644
--- a/Lib/fontTools/ttLib/sfnt.py
+++ b/Lib/fontTools/ttLib/sfnt.py
@@ -553,8 +553,7 @@ class WOFFFlavorData():
reader.file.seek(reader.metaOffset)
rawData = reader.file.read(reader.metaLength)
assert len(rawData) == reader.metaLength
- import zlib
- data = zlib.decompress(rawData)
+ data = self._decompress(rawData)
assert len(data) == reader.metaOrigLength
self.metaData = data
if reader.privLength:
@@ -563,6 +562,10 @@ class WOFFFlavorData():
assert len(data) == reader.privLength
self.privData = data
+ def _decompress(self, rawData):
+ import zlib
+ return zlib.decompress(rawData)
+
def calcChecksum(data):
"""Calculate the checksum for an arbitrary block of data.
diff --git a/Lib/fontTools/ttLib/tables/E_B_L_C_.py b/Lib/fontTools/ttLib/tables/E_B_L_C_.py
index f71ae95e..b065df07 100644
--- a/Lib/fontTools/ttLib/tables/E_B_L_C_.py
+++ b/Lib/fontTools/ttLib/tables/E_B_L_C_.py
@@ -154,7 +154,7 @@ class table_E_B_L_C_(DefaultTable.DefaultTable):
# (2) Build each bitmapSizeTable.
# (3) Consolidate all the data into the main dataList in the correct order.
- for curStrike in self.strikes:
+ for _ in self.strikes:
dataSize += sstruct.calcsize(bitmapSizeTableFormatPart1)
dataSize += len(('hori', 'vert')) * sstruct.calcsize(sbitLineMetricsFormat)
dataSize += sstruct.calcsize(bitmapSizeTableFormatPart2)
diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
index cd323f65..94417071 100644
--- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py
+++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py
@@ -166,7 +166,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable):
writer.simpletag("reserved", value=self.reserved)
writer.newline()
axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
- for glyphName in ttFont.getGlyphOrder():
+ for glyphName in ttFont.getGlyphNames():
variations = self.variations.get(glyphName)
if not variations:
continue
diff --git a/Lib/fontTools/ttLib/tables/_m_e_t_a.py b/Lib/fontTools/ttLib/tables/_m_e_t_a.py
index de544204..2cd479c9 100644
--- a/Lib/fontTools/ttLib/tables/_m_e_t_a.py
+++ b/Lib/fontTools/ttLib/tables/_m_e_t_a.py
@@ -85,7 +85,11 @@ class table__m_e_t_a(DefaultTable.DefaultTable):
else:
writer.begintag("hexdata", tag=tag)
writer.newline()
- writer.dumphex(self.data[tag])
+ data = self.data[tag]
+ if min(data) >= 0x20 and max(data) <= 0x7E:
+ writer.comment("ascii: " + data.decode("ascii"))
+ writer.newline()
+ writer.dumphex(data)
writer.endtag("hexdata")
writer.newline()
diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py
index e9ff2151..ec5d07ee 100644
--- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py
+++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py
@@ -184,6 +184,57 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
raise ValueError("nameID must be less than 32768")
return nameID
+ def findMultilingualName(self, names, windows=True, mac=True):
+ """Return the name ID of an existing multilingual name that
+ matches the 'names' dictionary, or None if not found.
+
+ 'names' is a dictionary with the name in multiple languages,
+ such as {'en': 'Pale', 'de': 'Blaß', 'de-CH': 'Blass'}.
+ The keys can be arbitrary IETF BCP 47 language codes;
+ the values are Unicode strings.
+
+ If 'windows' is True, the returned name ID is guaranteed
+ exist for all requested languages for platformID=3 and
+ platEncID=1.
+ If 'mac' is True, the returned name ID is guaranteed to exist
+ for all requested languages for platformID=1 and platEncID=0.
+ """
+ # Gather the set of requested
+ # (string, platformID, platEncID, langID)
+ # tuples
+ reqNameSet = set()
+ for lang, name in sorted(names.items()):
+ if windows:
+ windowsName = _makeWindowsName(name, None, lang)
+ if windowsName is not None:
+ reqNameSet.add((windowsName.string,
+ windowsName.platformID,
+ windowsName.platEncID,
+ windowsName.langID))
+ if mac:
+ macName = _makeMacName(name, None, lang)
+ if macName is not None:
+ reqNameSet.add((macName.string,
+ macName.platformID,
+ macName.platEncID,
+ macName.langID))
+
+ # Collect matching name IDs
+ matchingNames = dict()
+ for name in self.names:
+ key = (name.string, name.platformID,
+ name.platEncID, name.langID)
+ if key in reqNameSet:
+ nameSet = matchingNames.setdefault(name.nameID, set())
+ nameSet.add(key)
+
+ # Return the first name ID that defines all requested strings
+ for nameID, nameSet in sorted(matchingNames.items()):
+ if nameSet == reqNameSet:
+ return nameID
+
+ return None # not found
+
def addMultilingualName(self, names, ttFont=None, nameID=None,
windows=True, mac=True):
"""Add a multilingual name, returning its name ID
@@ -199,7 +250,8 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
names that otherwise cannot get encoded at all.
'nameID' is the name ID to be used, or None to let the library
- pick an unused name ID.
+ find an existing set of name records that match, or pick an
+ unused name ID.
If 'windows' is True, a platformID=3 name record will be added.
If 'mac' is True, a platformID=1 name record will be added.
@@ -207,6 +259,10 @@ class table__n_a_m_e(DefaultTable.DefaultTable):
if not hasattr(self, 'names'):
self.names = []
if nameID is None:
+ # Reuse nameID if possible
+ nameID = self.findMultilingualName(names, windows=windows, mac=mac)
+ if nameID is not None:
+ return nameID
nameID = self._findUnusedNameID()
# TODO: Should minimize BCP 47 language codes.
# https://github.com/fonttools/fonttools/issues/930
diff --git a/Lib/fontTools/ttLib/woff2.py b/Lib/fontTools/ttLib/woff2.py
index 849bf0ff..e77ad9a4 100644
--- a/Lib/fontTools/ttLib/woff2.py
+++ b/Lib/fontTools/ttLib/woff2.py
@@ -1168,26 +1168,8 @@ class WOFF2FlavorData(WOFFFlavorData):
raise ValueError(
"'glyf' and 'loca' must be transformed (or not) together"
)
-
- self.majorVersion = None
- self.minorVersion = None
- self.metaData = None
- self.privData = None
+ super(WOFF2FlavorData, self).__init__(reader=reader)
if reader:
- self.majorVersion = reader.majorVersion
- self.minorVersion = reader.minorVersion
- if reader.metaLength:
- reader.file.seek(reader.metaOffset)
- rawData = reader.file.read(reader.metaLength)
- assert len(rawData) == reader.metaLength
- metaData = brotli.decompress(rawData)
- assert len(metaData) == reader.metaOrigLength
- self.metaData = metaData
- if reader.privLength:
- reader.file.seek(reader.privOffset)
- privData = reader.file.read(reader.privLength)
- assert len(privData) == reader.privLength
- self.privData = privData
transformedTables = [
tag
for tag, entry in reader.tables.items()
@@ -1206,6 +1188,9 @@ class WOFF2FlavorData(WOFFFlavorData):
self.transformedTables = set(transformedTables)
+ def _decompress(self, rawData):
+ return brotli.decompress(rawData)
+
def unpackBase128(data):
r""" Read one to five bytes from UIntBase128-encoded input string, and return
@@ -1405,10 +1390,22 @@ def decompress(input_file, output_file):
def main(args=None):
+ """Compress and decompress WOFF2 fonts"""
import argparse
from fontTools import configLogger
from fontTools.ttx import makeOutputFileName
+ class _HelpAction(argparse._HelpAction):
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ subparsers_actions = [
+ action for action in parser._actions
+ if isinstance(action, argparse._SubParsersAction)]
+ for subparsers_action in subparsers_actions:
+ for choice, subparser in subparsers_action.choices.items():
+ print(subparser.format_help())
+ parser.exit()
+
class _NoGlyfTransformAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
namespace.transform_tables.difference_update({"glyf", "loca"})
@@ -1419,12 +1416,18 @@ def main(args=None):
parser = argparse.ArgumentParser(
prog="fonttools ttLib.woff2",
- description="Compress and decompress WOFF2 fonts",
+ description=main.__doc__,
+ add_help = False
)
+ parser.add_argument('-h', '--help', action=_HelpAction,
+ help='show this help message and exit')
+
parser_group = parser.add_subparsers(title="sub-commands")
- parser_compress = parser_group.add_parser("compress")
- parser_decompress = parser_group.add_parser("decompress")
+ parser_compress = parser_group.add_parser("compress",
+ description = "Compress a TTF or OTF font to WOFF2")
+ parser_decompress = parser_group.add_parser("decompress",
+ description = "Decompress a WOFF2 font to OTF")
for subparser in (parser_compress, parser_decompress):
group = subparser.add_mutually_exclusive_group(required=False)
diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py
index faacbedb..9522c625 100644
--- a/Lib/fontTools/ttx.py
+++ b/Lib/fontTools/ttx.py
@@ -384,6 +384,7 @@ def waitForKeyPress():
def main(args=None):
+ """Convert OpenType fonts to XML and back"""
from fontTools import configLogger
if args is None:
diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py
index 4b864378..d7f0ba5b 100755
--- a/Lib/fontTools/ufoLib/__init__.py
+++ b/Lib/fontTools/ufoLib/__init__.py
@@ -14,13 +14,12 @@ import fs.osfs
import fs.zipfs
import fs.tempfs
import fs.tools
-from fontTools.misc.py23 import tostr
from fontTools.misc import plistlib
from fontTools.ufoLib.validators import *
from fontTools.ufoLib.filenames import userNameToFileName
from fontTools.ufoLib.converters import convertUFO1OrUFO2KerningToUFO3Kerning
from fontTools.ufoLib.errors import UFOLibError
-from fontTools.ufoLib.utils import numberTypes
+from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
"""
A library for importing .ufo files and their descendants.
@@ -93,7 +92,11 @@ LAYERINFO_FILENAME = "layerinfo.plist"
DEFAULT_LAYER_NAME = "public.default"
-supportedUFOFormatVersions = [1, 2, 3]
+
+class UFOFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
+ FORMAT_1_0 = (1, 0)
+ FORMAT_2_0 = (2, 0)
+ FORMAT_3_0 = (3, 0)
class UFOFileStructure(enum.Enum):
@@ -264,9 +267,14 @@ class UFOReader(_UFOBaseIO):
)
self._path = fsdecode(path)
self._validate = validate
- self.readMetaInfo(validate=validate)
self._upConvertedKerningData = None
+ try:
+ self.readMetaInfo(validate=validate)
+ except UFOLibError:
+ self.close()
+ raise
+
# properties
def _get_path(self):
@@ -282,9 +290,26 @@ class UFOReader(_UFOBaseIO):
path = property(_get_path, doc="The path of the UFO (DEPRECATED).")
def _get_formatVersion(self):
- return self._formatVersion
+ import warnings
+
+ warnings.warn(
+ "The 'formatVersion' attribute is deprecated; use the 'formatVersionTuple'",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._formatVersion.major
+
+ formatVersion = property(
+ _get_formatVersion,
+ doc="The (major) format version of the UFO. DEPRECATED: Use formatVersionTuple"
+ )
- formatVersion = property(_get_formatVersion, doc="The format version of the UFO. This is determined by reading metainfo.plist during __init__.")
+ @property
+ def formatVersionTuple(self):
+ """The (major, minor) format version of the UFO.
+ This is determined by reading metainfo.plist during __init__.
+ """
+ return self._formatVersion
def _get_fileStructure(self):
return self._fileStructure
@@ -380,9 +405,9 @@ class UFOReader(_UFOBaseIO):
return None
# metainfo.plist
- def readMetaInfo(self, validate=None):
+ def _readMetaInfo(self, validate=None):
"""
- Read metainfo.plist. Only used for internal operations.
+ Read metainfo.plist and return raw data. Only used for internal operations.
``validate`` will validate the read data, by default it is set
to the class's validate value, can be overridden.
@@ -392,19 +417,44 @@ class UFOReader(_UFOBaseIO):
data = self._getPlist(METAINFO_FILENAME)
if validate and not isinstance(data, dict):
raise UFOLibError("metainfo.plist is not properly formatted.")
- formatVersion = data["formatVersion"]
- if validate:
- if not isinstance(formatVersion, int):
- raise UFOLibError(
- "formatVersion must be specified as an integer in '%s' on %s"
- % (METAINFO_FILENAME, self.fs)
- )
- if formatVersion not in supportedUFOFormatVersions:
- raise UFOLibError(
- "Unsupported UFO format (%d) in '%s' on %s"
- % (formatVersion, METAINFO_FILENAME, self.fs)
- )
- self._formatVersion = formatVersion
+ try:
+ formatVersionMajor = data["formatVersion"]
+ except KeyError:
+ raise UFOLibError(
+ f"Missing required formatVersion in '{METAINFO_FILENAME}' on {self.fs}"
+ )
+ formatVersionMinor = data.setdefault("formatVersionMinor", 0)
+
+ try:
+ formatVersion = UFOFormatVersion((formatVersionMajor, formatVersionMinor))
+ except ValueError as e:
+ unsupportedMsg = (
+ f"Unsupported UFO format ({formatVersionMajor}.{formatVersionMinor}) "
+ f"in '{METAINFO_FILENAME}' on {self.fs}"
+ )
+ if validate:
+ from fontTools.ufoLib.errors import UnsupportedUFOFormat
+
+ raise UnsupportedUFOFormat(unsupportedMsg) from e
+
+ formatVersion = UFOFormatVersion.default()
+ logger.warning(
+ "%s. Assuming the latest supported version (%s). "
+ "Some data may be skipped or parsed incorrectly",
+ unsupportedMsg, formatVersion
+ )
+ data["formatVersionTuple"] = formatVersion
+ return data
+
+ def readMetaInfo(self, validate=None):
+ """
+ Read metainfo.plist and set formatVersion. Only used for internal operations.
+
+ ``validate`` will validate the read data, by default it is set
+ to the class's validate value, can be overridden.
+ """
+ data = self._readMetaInfo(validate=validate)
+ self._formatVersion = data["formatVersionTuple"]
# groups.plist
@@ -420,7 +470,7 @@ class UFOReader(_UFOBaseIO):
if validate is None:
validate = self._validate
# handle up conversion
- if self._formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
self._upConvertKerning(validate)
groups = self._upConvertedKerningData["groups"]
# normal
@@ -451,7 +501,7 @@ class UFOReader(_UFOBaseIO):
"""
if validate is None:
validate = self._validate
- if self._formatVersion >= 3:
+ if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
return dict(side1={}, side2={})
# use the public group reader to force the load and
# conversion of the data if it hasn't happened yet.
@@ -481,7 +531,7 @@ class UFOReader(_UFOBaseIO):
infoDict = self._readInfo(validate)
infoDataToSet = {}
# version 1
- if self._formatVersion == 1:
+ if self._formatVersion == UFOFormatVersion.FORMAT_1_0:
for attr in fontInfoAttributesVersion1:
value = infoDict.get(attr)
if value is not None:
@@ -489,15 +539,15 @@ class UFOReader(_UFOBaseIO):
infoDataToSet = _convertFontInfoDataVersion1ToVersion2(infoDataToSet)
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
# version 2
- elif self._formatVersion == 2:
+ elif self._formatVersion == UFOFormatVersion.FORMAT_2_0:
for attr, dataValidationDict in list(fontInfoAttributesVersion2ValueData.items()):
value = infoDict.get(attr)
if value is None:
continue
infoDataToSet[attr] = value
infoDataToSet = _convertFontInfoDataVersion2ToVersion3(infoDataToSet)
- # version 3
- elif self._formatVersion == 3:
+ # version 3.x
+ elif self._formatVersion.major == UFOFormatVersion.FORMAT_3_0.major:
for attr, dataValidationDict in list(fontInfoAttributesVersion3ValueData.items()):
value = infoDict.get(attr)
if value is None:
@@ -505,7 +555,7 @@ class UFOReader(_UFOBaseIO):
infoDataToSet[attr] = value
# unsupported version
else:
- raise NotImplementedError
+ raise NotImplementedError(self._formatVersion)
# validate data
if validate:
infoDataToSet = validateInfoVersion3Data(infoDataToSet)
@@ -532,7 +582,7 @@ class UFOReader(_UFOBaseIO):
if validate is None:
validate = self._validate
# handle up conversion
- if self._formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
self._upConvertKerning(validate)
kerningNested = self._upConvertedKerningData["kerning"]
# normal
@@ -590,7 +640,7 @@ class UFOReader(_UFOBaseIO):
``validate`` will validate the layer contents.
"""
- if self._formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return [(DEFAULT_LAYER_NAME, DEFAULT_GLYPHS_DIRNAME)]
contents = self._getPlist(LAYERCONTENTS_FILENAME)
if validate:
@@ -722,7 +772,7 @@ class UFOReader(_UFOBaseIO):
``validate`` will validate the data, by default it is set to the
class's validate value, can be overridden.
"""
- if self._formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return []
if validate is None:
validate = self._validate
@@ -772,8 +822,10 @@ class UFOReader(_UFOBaseIO):
"""
if validate is None:
validate = self._validate
- if self._formatVersion < 3:
- raise UFOLibError("Reading images is not allowed in UFO %d." % self._formatVersion)
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
+ raise UFOLibError(
+ f"Reading images is not allowed in UFO {self._formatVersion.major}."
+ )
fileName = fsdecode(fileName)
try:
try:
@@ -813,18 +865,30 @@ class UFOWriter(UFOReader):
By default, the written data will be validated before writing. Set ``validate`` to
``False`` if you do not want to validate the data. Validation can also be overriden
on a per method level if desired.
+
+ The ``formatVersion`` argument allows to specify the UFO format version as a tuple
+ of integers (major, minor), or as a single integer for the major digit only (minor
+ is implied as 0). By default the latest formatVersion will be used; currently it's
+ 3.0, which is equivalent to formatVersion=(3, 0).
+
+ An UnsupportedUFOFormat exception is raised if the requested UFO formatVersion is
+ not supported.
"""
def __init__(
self,
path,
- formatVersion=3,
+ formatVersion=None,
fileCreator="com.github.fonttools.ufoLib",
structure=None,
validate=True,
):
- if formatVersion not in supportedUFOFormatVersions:
- raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
+ try:
+ formatVersion = UFOFormatVersion(formatVersion)
+ except ValueError as e:
+ from fontTools.ufoLib.errors import UnsupportedUFOFormat
+
+ raise UnsupportedUFOFormat(f"Unsupported UFO format: {formatVersion!r}") from e
if hasattr(path, "__fspath__"): # support os.PathLike objects
path = path.__fspath__()
@@ -940,22 +1004,20 @@ class UFOWriter(UFOReader):
# this will be needed for up and down conversion.
previousFormatVersion = None
if self._havePreviousFile:
- metaInfo = self._getPlist(METAINFO_FILENAME)
- previousFormatVersion = metaInfo.get("formatVersion")
- try:
- previousFormatVersion = int(previousFormatVersion)
- except (ValueError, TypeError):
- self.fs.close()
- raise UFOLibError("The existing metainfo.plist is not properly formatted.")
- if previousFormatVersion not in supportedUFOFormatVersions:
- self.fs.close()
- raise UFOLibError("Unsupported UFO format (%d)." % formatVersion)
- # catch down conversion
- if previousFormatVersion is not None and previousFormatVersion > formatVersion:
- raise UFOLibError("The UFO located at this path is a higher version (%d) than the version (%d) that is trying to be written. This is not supported." % (previousFormatVersion, formatVersion))
+ metaInfo = self._readMetaInfo(validate=validate)
+ previousFormatVersion = metaInfo["formatVersionTuple"]
+ # catch down conversion
+ if previousFormatVersion > formatVersion:
+ from fontTools.ufoLib.errors import UnsupportedUFOFormat
+
+ raise UnsupportedUFOFormat(
+ "The UFO located at this path is a higher version "
+ f"({previousFormatVersion}) than the version ({formatVersion}) "
+ "that is trying to be written. This is not supported."
+ )
# handle the layer contents
self.layerContents = {}
- if previousFormatVersion is not None and previousFormatVersion >= 3:
+ if previousFormatVersion is not None and previousFormatVersion.major >= 3:
# already exists
self.layerContents = OrderedDict(self._readLayerContents(validate))
else:
@@ -1091,8 +1153,10 @@ class UFOWriter(UFOReader):
def _writeMetaInfo(self):
metaInfo = dict(
creator=self._fileCreator,
- formatVersion=self._formatVersion
+ formatVersion=self._formatVersion.major,
)
+ if self._formatVersion.minor != 0:
+ metaInfo["formatVersionMinor"] = self._formatVersion.minor
self._writePlist(METAINFO_FILENAME, metaInfo)
# groups.plist
@@ -1113,7 +1177,7 @@ class UFOWriter(UFOReader):
This is the same form returned by UFOReader's
getKerningGroupConversionRenameMaps method.
"""
- if self._formatVersion >= 3:
+ if self._formatVersion >= UFOFormatVersion.FORMAT_3_0:
return # XXX raise an error here
# flip the dictionaries
remap = {}
@@ -1138,7 +1202,10 @@ class UFOWriter(UFOReader):
if not valid:
raise UFOLibError(message)
# down convert
- if self._formatVersion < 3 and self._downConversionKerningData is not None:
+ if (
+ self._formatVersion < UFOFormatVersion.FORMAT_3_0
+ and self._downConversionKerningData is not None
+ ):
remap = self._downConversionKerningData["groupRenameMap"]
remappedGroups = {}
# there are some edge cases here that are ignored:
@@ -1199,20 +1266,21 @@ class UFOWriter(UFOReader):
continue
infoData[attr] = value
# down convert data if necessary and validate
- if self._formatVersion == 3:
+ if self._formatVersion == UFOFormatVersion.FORMAT_3_0:
if validate:
infoData = validateInfoVersion3Data(infoData)
- elif self._formatVersion == 2:
+ elif self._formatVersion == UFOFormatVersion.FORMAT_2_0:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
- elif self._formatVersion == 1:
+ elif self._formatVersion == UFOFormatVersion.FORMAT_1_0:
infoData = _convertFontInfoDataVersion3ToVersion2(infoData)
if validate:
infoData = validateInfoVersion2Data(infoData)
infoData = _convertFontInfoDataVersion2ToVersion1(infoData)
- # write file
- self._writePlist(FONTINFO_FILENAME, infoData)
+ # write file if there is anything to write
+ if infoData:
+ self._writePlist(FONTINFO_FILENAME, infoData)
# kerning.plist
@@ -1248,7 +1316,10 @@ class UFOWriter(UFOReader):
if not isinstance(value, numberTypes):
raise UFOLibError(invalidFormatMessage)
# down convert
- if self._formatVersion < 3 and self._downConversionKerningData is not None:
+ if (
+ self._formatVersion < UFOFormatVersion.FORMAT_3_0
+ and self._downConversionKerningData is not None
+ ):
remap = self._downConversionKerningData["groupRenameMap"]
remappedKerning = {}
for (side1, side2), value in list(kerning.items()):
@@ -1298,7 +1369,7 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
- if self._formatVersion == 1:
+ if self._formatVersion == UFOFormatVersion.FORMAT_1_0:
raise UFOLibError("features.fea is not allowed in UFO Format Version 1.")
if validate:
if not isinstance(features, str):
@@ -1317,7 +1388,7 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
- if self.formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
return
if layerOrder is not None:
newOrder = []
@@ -1365,8 +1436,13 @@ class UFOWriter(UFOReader):
if validateWrite is None:
validateWrite = self._validate
# only default can be written in < 3
- if self._formatVersion < 3 and (not defaultLayer or layerName is not None):
- raise UFOLibError("Only the default layer can be writen in UFO %d." % self.formatVersion)
+ if (
+ self._formatVersion < UFOFormatVersion.FORMAT_3_0
+ and (not defaultLayer or layerName is not None)
+ ):
+ raise UFOLibError(
+ f"Only the default layer can be writen in UFO {self._formatVersion.major}."
+ )
# locate a layer name when None has been given
if layerName is None and defaultLayer:
for existingLayerName, directory in self.layerContents.items():
@@ -1377,40 +1453,39 @@ class UFOWriter(UFOReader):
elif layerName is None and not defaultLayer:
raise UFOLibError("A layer name must be provided for non-default layers.")
# move along to format specific writing
- if self.formatVersion == 1:
- return self._getGlyphSetFormatVersion1(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
- elif self.formatVersion == 2:
- return self._getGlyphSetFormatVersion2(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
- elif self.formatVersion == 3:
- return self._getGlyphSetFormatVersion3(validateRead, validateWrite, layerName=layerName, defaultLayer=defaultLayer, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
+ return self._getDefaultGlyphSet(validateRead, validateWrite, glyphNameToFileNameFunc=glyphNameToFileNameFunc)
+ elif self._formatVersion.major == UFOFormatVersion.FORMAT_3_0.major:
+ return self._getGlyphSetFormatVersion3(
+ validateRead,
+ validateWrite,
+ layerName=layerName,
+ defaultLayer=defaultLayer,
+ glyphNameToFileNameFunc=glyphNameToFileNameFunc,
+ )
else:
- raise AssertionError(self.formatVersion)
+ raise NotImplementedError(self._formatVersion)
- def _getGlyphSetFormatVersion1(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
+ def _getDefaultGlyphSet(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
from fontTools.ufoLib.glifLib import GlyphSet
glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
return GlyphSet(
glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
- ufoFormatVersion=1,
- validateRead=validateRead,
- validateWrite=validateWrite,
- )
-
- def _getGlyphSetFormatVersion2(self, validateRead, validateWrite, glyphNameToFileNameFunc=None):
- from fontTools.ufoLib.glifLib import GlyphSet
-
- glyphSubFS = self.fs.makedir(DEFAULT_GLYPHS_DIRNAME, recreate=True)
- return GlyphSet(
- glyphSubFS,
- glyphNameToFileNameFunc=glyphNameToFileNameFunc,
- ufoFormatVersion=2,
+ ufoFormatVersion=self._formatVersion,
validateRead=validateRead,
validateWrite=validateWrite,
)
- def _getGlyphSetFormatVersion3(self, validateRead, validateWrite, layerName=None, defaultLayer=True, glyphNameToFileNameFunc=None):
+ def _getGlyphSetFormatVersion3(
+ self,
+ validateRead,
+ validateWrite,
+ layerName=None,
+ defaultLayer=True,
+ glyphNameToFileNameFunc=None,
+ ):
from fontTools.ufoLib.glifLib import GlyphSet
# if the default flag is on, make sure that the default in the file
@@ -1446,7 +1521,7 @@ class UFOWriter(UFOReader):
return GlyphSet(
glyphSubFS,
glyphNameToFileNameFunc=glyphNameToFileNameFunc,
- ufoFormatVersion=3,
+ ufoFormatVersion=self._formatVersion,
validateRead=validateRead,
validateWrite=validateWrite,
)
@@ -1459,7 +1534,7 @@ class UFOWriter(UFOReader):
layerName, it is up to the caller to inform that object that
the directory it represents has changed.
"""
- if self._formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
# ignore renaming glyph sets for UFO1 UFO2
# just write the data from the default layer
return
@@ -1498,7 +1573,7 @@ class UFOWriter(UFOReader):
"""
Remove the glyph set matching layerName.
"""
- if self._formatVersion < 3:
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
# ignore deleting glyph sets for UFO1 UFO2 as there are no layers
# just write the data from the default layer
return
@@ -1528,8 +1603,10 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
- if self._formatVersion < 3:
- raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
+ raise UFOLibError(
+ f"Images are not allowed in UFO {self._formatVersion.major}."
+ )
fileName = fsdecode(fileName)
if validate:
valid, error = pngValidator(data=data)
@@ -1542,8 +1619,10 @@ class UFOWriter(UFOReader):
Remove the file named fileName from the
images directory.
"""
- if self._formatVersion < 3:
- raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
+ raise UFOLibError(
+ f"Images are not allowed in UFO {self._formatVersion.major}."
+ )
self.removePath(f"{IMAGES_DIRNAME}/{fsdecode(fileName)}")
def copyImageFromReader(self, reader, sourceFileName, destFileName, validate=None):
@@ -1554,8 +1633,10 @@ class UFOWriter(UFOReader):
"""
if validate is None:
validate = self._validate
- if self._formatVersion < 3:
- raise UFOLibError("Images are not allowed in UFO %d." % self._formatVersion)
+ if self._formatVersion < UFOFormatVersion.FORMAT_3_0:
+ raise UFOLibError(
+ f"Images are not allowed in UFO {self._formatVersion.major}."
+ )
sourcePath = f"{IMAGES_DIRNAME}/{fsdecode(sourceFileName)}"
destPath = f"{IMAGES_DIRNAME}/{fsdecode(destFileName)}"
self.copyFromReader(reader, sourcePath, destPath)
diff --git a/Lib/fontTools/ufoLib/errors.py b/Lib/fontTools/ufoLib/errors.py
index 53a65bfd..304345e4 100644
--- a/Lib/fontTools/ufoLib/errors.py
+++ b/Lib/fontTools/ufoLib/errors.py
@@ -4,5 +4,13 @@ class UFOLibError(Exception):
pass
+class UnsupportedUFOFormat(UFOLibError):
+ pass
+
+
class GlifLibError(UFOLibError):
pass
+
+
+class UnsupportedGLIFFormat(GlifLibError):
+ pass
diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py
index b18ac489..16fa906a 100755
--- a/Lib/fontTools/ufoLib/glifLib.py
+++ b/Lib/fontTools/ufoLib/glifLib.py
@@ -10,6 +10,8 @@ in a folder. It offers two ways to read glyph data, and one way to write
glyph data. See the class doc string for details.
"""
+import logging
+import enum
from warnings import warn
from collections import OrderedDict
import fs
@@ -32,8 +34,8 @@ from fontTools.ufoLib.validators import (
glyphLibValidator,
)
from fontTools.misc import etree
-from fontTools.ufoLib import _UFOBaseIO
-from fontTools.ufoLib.utils import numberTypes
+from fontTools.ufoLib import _UFOBaseIO, UFOFormatVersion
+from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin
__all__ = [
@@ -43,6 +45,8 @@ __all__ = [
"glyphNameToFileName"
]
+logger = logging.getLogger(__name__)
+
# ---------
# Constants
@@ -50,8 +54,28 @@ __all__ = [
CONTENTS_FILENAME = "contents.plist"
LAYERINFO_FILENAME = "layerinfo.plist"
-supportedUFOFormatVersions = [1, 2, 3]
-supportedGLIFFormatVersions = [1, 2]
+
+
+class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum):
+ FORMAT_1_0 = (1, 0)
+ FORMAT_2_0 = (2, 0)
+
+ @classmethod
+ def default(cls, ufoFormatVersion=None):
+ if ufoFormatVersion is not None:
+ return max(cls.supported_versions(ufoFormatVersion))
+ return super().default()
+
+ @classmethod
+ def supported_versions(cls, ufoFormatVersion=None):
+ if ufoFormatVersion is None:
+ # if ufo format unspecified, return all the supported GLIF formats
+ return super().supported_versions()
+ # else only return the GLIF formats supported by the given UFO format
+ versions = {cls.FORMAT_1_0}
+ if ufoFormatVersion >= UFOFormatVersion.FORMAT_3_0:
+ versions.add(cls.FORMAT_2_0)
+ return frozenset(versions)
# ------------
@@ -108,7 +132,7 @@ class GlyphSet(_UFOBaseIO):
self,
path,
glyphNameToFileNameFunc=None,
- ufoFormatVersion=3,
+ ufoFormatVersion=None,
validateRead=True,
validateWrite=True,
):
@@ -125,8 +149,18 @@ class GlyphSet(_UFOBaseIO):
``validateRead`` will validate read operations. Its default is ``True``.
``validateWrite`` will validate write operations. Its default is ``True``.
"""
- if ufoFormatVersion not in supportedUFOFormatVersions:
- raise GlifLibError("Unsupported UFO format version: %s" % ufoFormatVersion)
+ try:
+ ufoFormatVersion = UFOFormatVersion(ufoFormatVersion)
+ except ValueError as e:
+ from fontTools.ufoLib.errors import UnsupportedUFOFormat
+
+ raise UnsupportedUFOFormat(
+ f"Unsupported UFO format: {ufoFormatVersion!r}"
+ ) from e
+
+ if hasattr(path, "__fspath__"): # support os.PathLike objects
+ path = path.__fspath__()
+
if isinstance(path, str):
try:
filesystem = fs.osfs.OSFS(path)
@@ -157,7 +191,9 @@ class GlyphSet(_UFOBaseIO):
self.fs = filesystem
# if glyphSet contains no 'contents.plist', we consider it empty
self._havePreviousFile = filesystem.exists(CONTENTS_FILENAME)
- self.ufoFormatVersion = ufoFormatVersion
+ # attribute kept for backward compatibility
+ self.ufoFormatVersion = ufoFormatVersion.major
+ self.ufoFormatVersionTuple = ufoFormatVersion
if glyphNameToFileNameFunc is None:
glyphNameToFileNameFunc = glyphNameToFileName
self.glyphNameToFileName = glyphNameToFileNameFunc
@@ -251,8 +287,10 @@ class GlyphSet(_UFOBaseIO):
"""
if validateWrite is None:
validateWrite = self._validateWrite
- if self.ufoFormatVersion < 3:
- raise GlifLibError("layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersion)
+ if self.ufoFormatVersionTuple.major < 3:
+ raise GlifLibError(
+ "layerinfo.plist is not allowed in UFO %d." % self.ufoFormatVersionTuple.major
+ )
# gather data
infoData = {}
for attr in layerInfoVersion3ValueData.keys():
@@ -346,10 +384,7 @@ class GlyphSet(_UFOBaseIO):
validate = self._validateRead
text = self.getGLIF(glyphName)
tree = _glifTreeFromString(text)
- if self.ufoFormatVersion < 3:
- formatVersions = (1,)
- else:
- formatVersions = (1, 2)
+ formatVersions = GLIFFormatVersion.supported_versions(self.ufoFormatVersionTuple)
_readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
def writeGlyph(self, glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=None, validate=None):
@@ -379,21 +414,35 @@ class GlyphSet(_UFOBaseIO):
The GLIF format version will be chosen based on the ufoFormatVersion
passed during the creation of this object. If a particular format
version is desired, it can be passed with the formatVersion argument.
+ The formatVersion argument accepts either a tuple of integers for
+ (major, minor), or a single integer for the major digit only (with
+ minor digit implied as 0).
+
+ An UnsupportedGLIFFormat exception is raised if the requested GLIF
+ formatVersion is not supported.
``validate`` will validate the data, by default it is set to the
class's ``validateWrite`` value, can be overridden.
"""
if formatVersion is None:
- if self.ufoFormatVersion >= 3:
- formatVersion = 2
- else:
- formatVersion = 1
- if formatVersion not in supportedGLIFFormatVersions:
- raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
- if formatVersion == 2 and self.ufoFormatVersion < 3:
- raise GlifLibError(
- "Unsupported GLIF format version (%d) for UFO format version %d."
- % (formatVersion, self.ufoFormatVersion)
+ formatVersion = GLIFFormatVersion.default(self.ufoFormatVersionTuple)
+ else:
+ try:
+ formatVersion = GLIFFormatVersion(formatVersion)
+ except ValueError as e:
+ from fontTools.ufoLib.errors import UnsupportedGLIFFormat
+
+ raise UnsupportedGLIFFormat(
+ f"Unsupported GLIF format version: {formatVersion!r}"
+ ) from e
+ if formatVersion not in GLIFFormatVersion.supported_versions(
+ self.ufoFormatVersionTuple
+ ):
+ from fontTools.ufoLib.errors import UnsupportedGLIFFormat
+
+ raise UnsupportedGLIFFormat(
+ f"Unsupported GLIF format version ({formatVersion!s}) "
+ f"for UFO format version {self.ufoFormatVersionTuple!s}."
)
if validate is None:
validate = self._validateWrite
@@ -527,7 +576,13 @@ def glyphNameToFileName(glyphName, existingFileNames):
# GLIF To and From String
# -----------------------
-def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
+def readGlyphFromString(
+ aString,
+ glyphObject=None,
+ pointPen=None,
+ formatVersions=None,
+ validate=True,
+):
"""
Read .glif data from a string into a glyph object.
@@ -556,25 +611,65 @@ def readGlyphFromString(aString, glyphObject=None, pointPen=None, formatVersions
conforming to the PointPen protocol as the 'pointPen' argument.
This argument may be None if you don't need the outline data.
- The formatVersions argument defined the GLIF format versions
+ The formatVersions optional argument define the GLIF format versions
that are allowed to be read.
+ The type is Optional[Iterable[Tuple[int, int], int]]. It can contain
+ either integers (for the major versions to be allowed, with minor
+ digits defaulting to 0), or tuples of integers to specify both
+ (major, minor) versions.
+ By default when formatVersions is None all the GLIF format versions
+ currently defined are allowed to be read.
``validate`` will validate the read data. It is set to ``True`` by default.
"""
tree = _glifTreeFromString(aString)
- _readGlyphFromTree(tree, glyphObject, pointPen, formatVersions=formatVersions, validate=validate)
+
+ if formatVersions is None:
+ validFormatVersions = GLIFFormatVersion.supported_versions()
+ else:
+ validFormatVersions, invalidFormatVersions = set(), set()
+ for v in formatVersions:
+ try:
+ formatVersion = GLIFFormatVersion(v)
+ except ValueError:
+ invalidFormatVersions.add(v)
+ else:
+ validFormatVersions.add(formatVersion)
+ if not validFormatVersions:
+ raise ValueError(
+ "None of the requested GLIF formatVersions are supported: "
+ f"{formatVersions!r}"
+ )
+
+ _readGlyphFromTree(
+ tree, glyphObject, pointPen, formatVersions=validFormatVersions, validate=validate
+ )
def _writeGlyphToBytes(
- glyphName, glyphObject=None, drawPointsFunc=None, writer=None,
- formatVersion=2, validate=True):
+ glyphName,
+ glyphObject=None,
+ drawPointsFunc=None,
+ writer=None,
+ formatVersion=None,
+ validate=True,
+):
"""Return .glif data for a glyph as a UTF-8 encoded bytes string."""
+ try:
+ formatVersion = GLIFFormatVersion(formatVersion)
+ except ValueError:
+ from fontTools.ufoLib.errors import UnsupportedGLIFFormat
+
+ raise UnsupportedGLIFFormat("Unsupported GLIF format version: {formatVersion!r}")
# start
if validate and not isinstance(glyphName, str):
raise GlifLibError("The glyph name is not properly formatted.")
if validate and len(glyphName) == 0:
raise GlifLibError("The glyph name is empty.")
- root = etree.Element("glyph", OrderedDict([("name", glyphName), ("format", repr(formatVersion))]))
+ glyphAttrs = OrderedDict([("name", glyphName), ("format", repr(formatVersion.major))])
+ if formatVersion.minor != 0:
+ glyphAttrs["formatMinor"] = repr(formatVersion.minor)
+ root = etree.Element("glyph", glyphAttrs)
identifiers = set()
# advance
_writeAdvance(glyphObject, root, validate)
@@ -585,21 +680,21 @@ def _writeGlyphToBytes(
if getattr(glyphObject, "note", None):
_writeNote(glyphObject, root, validate)
# image
- if formatVersion >= 2 and getattr(glyphObject, "image", None):
+ if formatVersion.major >= 2 and getattr(glyphObject, "image", None):
_writeImage(glyphObject, root, validate)
# guidelines
- if formatVersion >= 2 and getattr(glyphObject, "guidelines", None):
+ if formatVersion.major >= 2 and getattr(glyphObject, "guidelines", None):
_writeGuidelines(glyphObject, root, identifiers, validate)
# anchors
anchors = getattr(glyphObject, "anchors", None)
- if formatVersion >= 2 and anchors:
+ if formatVersion.major >= 2 and anchors:
_writeAnchors(glyphObject, root, identifiers, validate)
# outline
if drawPointsFunc is not None:
outline = etree.SubElement(root, "outline")
pen = GLIFPointPen(outline, identifiers=identifiers, validate=validate)
drawPointsFunc(pen)
- if formatVersion == 1 and anchors:
+ if formatVersion.major == 1 and anchors:
_writeAnchorsFormat1(pen, anchors, validate)
# prevent lxml from writing self-closing tags
if not len(outline):
@@ -614,7 +709,13 @@ def _writeGlyphToBytes(
return data
-def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatVersion=2, validate=True):
+def writeGlyphToString(
+ glyphName,
+ glyphObject=None,
+ drawPointsFunc=None,
+ formatVersion=None,
+ validate=True,
+):
"""
Return .glif data for a glyph as a string. The XML declaration's
encoding is always set to "UTF-8".
@@ -640,6 +741,13 @@ def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, formatV
proper PointPen methods to transfer the outline to the .glif file.
The GLIF format version can be specified with the formatVersion argument.
+ This accepts either a tuple of integers for (major, minor), or a single
+ integer for the major digit only (with minor digit implied as 0).
+ By default when formatVesion is None the latest GLIF format version will
+ be used; currently it's 2.0, which is equivalent to formatVersion=(2, 0).
+
+ An UnsupportedGLIFFormat exception is raised if the requested UFO
+ formatVersion is not supported.
``validate`` will validate the written data. It is set to ``True`` by default.
"""
@@ -876,27 +984,54 @@ def _glifTreeFromString(aString):
raise GlifLibError("Invalid GLIF structure.")
return root
-def _readGlyphFromTree(tree, glyphObject=None, pointPen=None, formatVersions=(1, 2), validate=True):
+
+def _readGlyphFromTree(
+ tree,
+ glyphObject=None,
+ pointPen=None,
+ formatVersions=GLIFFormatVersion.supported_versions(),
+ validate=True,
+):
# check the format version
- formatVersion = tree.get("format")
- if validate and formatVersion is None:
+ formatVersionMajor = tree.get("format")
+ if validate and formatVersionMajor is None:
raise GlifLibError("Unspecified format version in GLIF.")
+ formatVersionMinor = tree.get("formatMinor", 0)
try:
- v = int(formatVersion)
- formatVersion = v
- except ValueError:
- pass
+ formatVersion = GLIFFormatVersion((int(formatVersionMajor), int(formatVersionMinor)))
+ except ValueError as e:
+ msg = "Unsupported GLIF format: %s.%s" % (formatVersionMajor, formatVersionMinor)
+ if validate:
+ from fontTools.ufoLib.errors import UnsupportedGLIFFormat
+
+ raise UnsupportedGLIFFormat(msg) from e
+ # warn but continue using the latest supported format
+ formatVersion = GLIFFormatVersion.default()
+ logger.warning(
+ "%s. Assuming the latest supported version (%s). "
+ "Some data may be skipped or parsed incorrectly.",
+ msg,
+ formatVersion,
+ )
+
if validate and formatVersion not in formatVersions:
- raise GlifLibError("Forbidden GLIF format version: %s" % formatVersion)
- if formatVersion == 1:
- _readGlyphFromTreeFormat1(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
- elif formatVersion == 2:
- _readGlyphFromTreeFormat2(tree=tree, glyphObject=glyphObject, pointPen=pointPen, validate=validate)
- else:
- raise GlifLibError("Unsupported GLIF format version: %s" % formatVersion)
+ raise GlifLibError(f"Forbidden GLIF format version: {formatVersion!s}")
+
+ try:
+ readGlyphFromTree = _READ_GLYPH_FROM_TREE_FUNCS[formatVersion]
+ except KeyError:
+ raise NotImplementedError(formatVersion)
+
+ readGlyphFromTree(
+ tree=tree,
+ glyphObject=glyphObject,
+ pointPen=pointPen,
+ validate=validate,
+ formatMinor=formatVersion.minor,
+ )
-def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None):
+def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=None, **kwargs):
# get the name
_readName(glyphObject, tree, validate)
# populate the sub elements
@@ -944,7 +1079,9 @@ def _readGlyphFromTreeFormat1(tree, glyphObject=None, pointPen=None, validate=No
if unicodes:
_relaxedSetattr(glyphObject, "unicodes", unicodes)
-def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=None):
+def _readGlyphFromTreeFormat2(
+ tree, glyphObject=None, pointPen=None, validate=None, formatMinor=0
+):
# get the name
_readName(glyphObject, tree, validate)
# populate the sub elements
@@ -1030,6 +1167,13 @@ def _readGlyphFromTreeFormat2(tree, glyphObject=None, pointPen=None, validate=No
raise GlifLibError("The anchors are improperly formatted.")
_relaxedSetattr(glyphObject, "anchors", anchors)
+
+_READ_GLYPH_FROM_TREE_FUNCS = {
+ GLIFFormatVersion.FORMAT_1_0: _readGlyphFromTreeFormat1,
+ GLIFFormatVersion.FORMAT_2_0: _readGlyphFromTreeFormat2,
+}
+
+
def _readName(glyphObject, root, validate):
glyphName = root.get("name")
if validate and not glyphName:
@@ -1512,10 +1656,10 @@ class GLIFPointPen(AbstractPointPen):
part of .glif files.
"""
- def __init__(self, element, formatVersion=2, identifiers=None, validate=True):
+ def __init__(self, element, formatVersion=None, identifiers=None, validate=True):
if identifiers is None:
identifiers = set()
- self.formatVersion = formatVersion
+ self.formatVersion = GLIFFormatVersion(formatVersion)
self.identifiers = identifiers
self.outline = element
self.contour = None
@@ -1525,7 +1669,7 @@ class GLIFPointPen(AbstractPointPen):
def beginPath(self, identifier=None, **kwargs):
attrs = OrderedDict()
- if identifier is not None and self.formatVersion >= 2:
+ if identifier is not None and self.formatVersion.major >= 2:
if self.validate:
if identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier)
@@ -1586,7 +1730,7 @@ class GLIFPointPen(AbstractPointPen):
if name is not None:
attrs["name"] = name
# identifier
- if identifier is not None and self.formatVersion >= 2:
+ if identifier is not None and self.formatVersion.major >= 2:
if self.validate:
if identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier)
@@ -1603,7 +1747,7 @@ class GLIFPointPen(AbstractPointPen):
raise GlifLibError("transformation values must be int or float")
if value != default:
attrs[attr] = repr(value)
- if identifier is not None and self.formatVersion >= 2:
+ if identifier is not None and self.formatVersion.major >= 2:
if self.validate:
if identifier in self.identifiers:
raise GlifLibError("identifier used more than once: %s" % identifier)
diff --git a/Lib/fontTools/ufoLib/utils.py b/Lib/fontTools/ufoLib/utils.py
index effc71fd..85878b47 100644
--- a/Lib/fontTools/ufoLib/utils.py
+++ b/Lib/fontTools/ufoLib/utils.py
@@ -36,6 +36,39 @@ def deprecated(msg=""):
return deprecated_decorator
+# To be mixed with enum.Enum in UFOFormatVersion and GLIFFormatVersion
+class _VersionTupleEnumMixin:
+ @property
+ def major(self):
+ return self.value[0]
+
+ @property
+ def minor(self):
+ return self.value[1]
+
+ @classmethod
+ def _missing_(cls, value):
+ # allow to initialize a version enum from a single (major) integer
+ if isinstance(value, int):
+ return cls((value, 0))
+ # or from None to obtain the current default version
+ if value is None:
+ return cls.default()
+ return super()._missing_(value)
+
+ def __str__(self):
+ return f"{self.major}.{self.minor}"
+
+ @classmethod
+ def default(cls):
+ # get the latest defined version (i.e. the max of all versions)
+ return max(cls.__members__.values())
+
+ @classmethod
+ def supported_versions(cls):
+ return frozenset(cls.__members__.values())
+
+
if __name__ == "__main__":
import doctest
diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py
index 46029697..404ad8b8 100644
--- a/Lib/fontTools/unicodedata/__init__.py
+++ b/Lib/fontTools/unicodedata/__init__.py
@@ -134,10 +134,8 @@ def script_code(script_name, default=KeyError):
return default
-# The data on script direction is taken from harfbuzz's "hb-common.cc":
-# https://goo.gl/X5FDXC
-# It matches the CLDR "scriptMetadata.txt as of January 2018:
-# http://unicode.org/repos/cldr/trunk/common/properties/scriptMetadata.txt
+# The data on script direction is taken from CLDR 37:
+# https://github.com/unicode-org/cldr/blob/release-37/common/properties/scriptMetadata.txt
RTL_SCRIPTS = {
# Unicode-1.1 additions
'Arab', # Arabic
@@ -198,6 +196,10 @@ RTL_SCRIPTS = {
# Unicode-12.0 additions
'Elym', # Elymaic
+
+ # Unicode-13.0 additions
+ 'Chrs', # Chorasmian
+ 'Yezi', # Yezidi
}
def script_horizontal_direction(script_code, default=KeyError):
diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py
index 862decac..1bf586f7 100644
--- a/Lib/fontTools/varLib/__init__.py
+++ b/Lib/fontTools/varLib/__init__.py
@@ -202,30 +202,10 @@ def _add_stat(font, axes):
if "STAT" in font:
return
+ from ..otlLib.builder import buildStatTable
fvarTable = font['fvar']
-
- STAT = font["STAT"] = newTable('STAT')
- stat = STAT.table = ot.STAT()
- stat.Version = 0x00010001
-
- axisRecords = []
- for i, a in enumerate(fvarTable.axes):
- axis = ot.AxisRecord()
- axis.AxisTag = Tag(a.axisTag)
- axis.AxisNameID = a.axisNameID
- axis.AxisOrdering = i
- axisRecords.append(axis)
-
- axisRecordArray = ot.AxisRecordArray()
- axisRecordArray.Axis = axisRecords
- # XXX these should not be hard-coded but computed automatically
- stat.DesignAxisRecordSize = 8
- stat.DesignAxisCount = len(axisRecords)
- stat.DesignAxisRecord = axisRecordArray
-
- # for the elided fallback name, we default to the base style name.
- # TODO make this user-configurable via designspace document
- stat.ElidedFallbackNameID = 2
+ axes = [dict(tag=a.axisTag, name=a.axisNameID) for a in fvarTable.axes]
+ buildStatTable(font, axes)
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
@@ -1027,10 +1007,11 @@ class MasterFinder(object):
def main(args=None):
+ """Build a variable font from a designspace file and masters"""
from argparse import ArgumentParser
from fontTools import configLogger
- parser = ArgumentParser(prog='varLib')
+ parser = ArgumentParser(prog='varLib', description = main.__doc__)
parser.add_argument('designspace')
parser.add_argument(
'-o',
diff --git a/Lib/fontTools/varLib/__main__.py b/Lib/fontTools/varLib/__main__.py
index 29657401..4b3a0f53 100644
--- a/Lib/fontTools/varLib/__main__.py
+++ b/Lib/fontTools/varLib/__main__.py
@@ -1,5 +1,6 @@
import sys
from fontTools.varLib import main
+
if __name__ == '__main__':
sys.exit(main())
diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py
index 000e1b34..4e2672b3 100644
--- a/Lib/fontTools/varLib/cff.py
+++ b/Lib/fontTools/varLib/cff.py
@@ -453,7 +453,7 @@ class MergeOutlineExtractor(CFFToCFF2OutlineExtractor):
def __init__(self, pen, localSubrs, globalSubrs,
nominalWidthX, defaultWidthX, private=None):
- super(CFFToCFF2OutlineExtractor, self).__init__(pen, localSubrs,
+ super().__init__(pen, localSubrs,
globalSubrs, nominalWidthX, defaultWidthX, private)
def countHints(self):
@@ -507,9 +507,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
def __init__(
self, default_commands, glyphName, num_masters, master_idx,
roundTolerance=0.5):
- super(
- CFF2CharStringMergePen,
- self).__init__(
+ super().__init__(
width=None,
glyphSet=None, CFF2=True,
roundTolerance=roundTolerance)
diff --git a/Lib/fontTools/varLib/featureVars.py b/Lib/fontTools/varLib/featureVars.py
index dab9a0b1..76e8cc4a 100644
--- a/Lib/fontTools/varLib/featureVars.py
+++ b/Lib/fontTools/varLib/featureVars.py
@@ -82,6 +82,7 @@ def overlayFeatureVariations(conditionalSubstitutions):
... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
... ([{"wght": (0.5, 1.0)}], {"dollar": "dollar.rvrn"}),
... ([{"wdth": (0.5, 1.0)}], {"cent": "cent.rvrn"}),
+ ... ([{"wght": (0.5, 1.0), "wdth": (-1, 1.0)}], {"dollar": "dollar.rvrn"}),
... ]
>>> from pprint import pprint
>>> pprint(overlayFeatureVariations(condSubst))
@@ -136,12 +137,14 @@ def overlayFeatureVariations(conditionalSubstitutions):
remainder = hashdict(remainder)
newMap[remainder] = newMap.get(remainder, 0) | rank
boxMap = newMap
- del boxMap[hashdict()]
# Generate output
items = []
for box,rank in sorted(boxMap.items(),
key=(lambda BoxAndRank: -popCount(BoxAndRank[1]))):
+ # Skip any box that doesn't have any substitution.
+ if rank == 0:
+ continue
substsList = []
i = 0
while rank:
diff --git a/Lib/fontTools/varLib/instancer.py b/Lib/fontTools/varLib/instancer.py
index f0cb646a..2d22d622 100644
--- a/Lib/fontTools/varLib/instancer.py
+++ b/Lib/fontTools/varLib/instancer.py
@@ -22,7 +22,7 @@ font, keeping only the deltas associated with the wdth axis:
| >>> from fontTools import ttLib
| >>> from fontTools.varLib import instancer
| >>> varfont = ttLib.TTFont("path/to/MyVariableFont.ttf")
-| >>> [a.axisTag for a in partial["fvar"].axes] # the varfont's current axes
+| >>> [a.axisTag for a in varfont["fvar"].axes] # the varfont's current axes
| ['wght', 'wdth']
| >>> partial = instancer.instantiateVariableFont(varfont, {"wght": 300})
| >>> [a.axisTag for a in partial["fvar"].axes] # axes left after pinning 'wght'
@@ -1375,6 +1375,7 @@ def parseArgs(args):
def main(args=None):
+ """Partially instantiate a variable font."""
infile, axisLimits, options = parseArgs(args)
log.info("Restricting axes: %s", axisLimits)
diff --git a/Lib/fontTools/varLib/interpolatable.py b/Lib/fontTools/varLib/interpolatable.py
index d4d7eeda..6488022f 100644
--- a/Lib/fontTools/varLib/interpolatable.py
+++ b/Lib/fontTools/varLib/interpolatable.py
@@ -157,22 +157,31 @@ def test(glyphsets, glyphs=None, names=None):
#for x in hist:
# print(x)
-def main(args):
- filenames = args
+def main(args=None):
+ """Test for interpolatability issues between fonts"""
+ import argparse
+ parser = argparse.ArgumentParser(
+ "fonttools varLib.interpolatable",
+ description=main.__doc__,
+ )
+ parser.add_argument('inputs', metavar='FILE', type=str, nargs='+',
+ help="Input TTF files")
+
+ args = parser.parse_args(args)
glyphs = None
#glyphs = ['uni08DB', 'uniFD76']
#glyphs = ['uni08DE', 'uni0034']
#glyphs = ['uni08DE', 'uni0034', 'uni0751', 'uni0753', 'uni0754', 'uni08A4', 'uni08A4.fina', 'uni08A5.fina']
from os.path import basename
- names = [basename(filename).rsplit('.', 1)[0] for filename in filenames]
+ names = [basename(filename).rsplit('.', 1)[0] for filename in args.inputs]
from fontTools.ttLib import TTFont
- fonts = [TTFont(filename) for filename in filenames]
+ fonts = [TTFont(filename) for filename in args.inputs]
glyphsets = [font.getGlyphSet() for font in fonts]
test(glyphsets, glyphs=glyphs, names=names)
if __name__ == '__main__':
import sys
- main(sys.argv[1:])
+ main()
diff --git a/Lib/fontTools/varLib/interpolate_layout.py b/Lib/fontTools/varLib/interpolate_layout.py
index d008e1ce..6d0385dd 100644
--- a/Lib/fontTools/varLib/interpolate_layout.py
+++ b/Lib/fontTools/varLib/interpolate_layout.py
@@ -58,29 +58,42 @@ def interpolate_layout(designspace, loc, master_finder=lambda s:s, mapped=False)
def main(args=None):
+ """Interpolate GDEF/GPOS/GSUB tables for a point on a designspace"""
from fontTools import configLogger
-
+ import argparse
import sys
- if args is None:
- args = sys.argv[1:]
- designspace_filename = args[0]
- locargs = args[1:]
- outfile = os.path.splitext(designspace_filename)[0] + '-instance.ttf'
+ parser = argparse.ArgumentParser(
+ "fonttools varLib.interpolate_layout",
+ description=main.__doc__,
+ )
+ parser.add_argument('designspace_filename', metavar='DESIGNSPACE',
+ help="Input TTF files")
+ parser.add_argument('locations', metavar='LOCATION', type=str, nargs='+',
+ help="Axis locations (e.g. wdth=120")
+ parser.add_argument('-o', '--output', metavar='OUTPUT',
+ help="Output font file (defaults to <designspacename>-instance.ttf)")
+ parser.add_argument('-l', '--loglevel', metavar='LEVEL', default="INFO",
+ help="Logging level (defaults to INFO)")
+
+
+ args = parser.parse_args(args)
+
+ if not args.output:
+ args.output = os.path.splitext(args.designspace_filename)[0] + '-instance.ttf'
- # TODO: allow user to configure logging via command-line options
- configLogger(level="INFO")
+ configLogger(level=args.loglevel)
finder = lambda s: s.replace('master_ufo', 'master_ttf_interpolatable').replace('.ufo', '.ttf')
loc = {}
- for arg in locargs:
+ for arg in args.locations:
tag,val = arg.split('=')
loc[tag] = float(val)
- font = interpolate_layout(designspace_filename, loc, finder)
- log.info("Saving font %s", outfile)
- font.save(outfile)
+ font = interpolate_layout(args.designspace_filename, loc, finder)
+ log.info("Saving font %s", args.output)
+ font.save(args.output)
if __name__ == "__main__":
diff --git a/Lib/fontTools/varLib/merger.py b/Lib/fontTools/varLib/merger.py
index b7fb39b4..071942b8 100644
--- a/Lib/fontTools/varLib/merger.py
+++ b/Lib/fontTools/varLib/merger.py
@@ -154,7 +154,7 @@ def _SinglePosUpgradeToFormat2(self):
ret.Format = 2
ret.Coverage = self.Coverage
ret.ValueFormat = self.ValueFormat
- ret.Value = [self.Value for g in ret.Coverage.glyphs]
+ ret.Value = [self.Value for _ in ret.Coverage.glyphs]
ret.ValueCount = len(ret.Value)
return ret
@@ -260,7 +260,7 @@ def merge(merger, self, lst):
[v.Value for v in lst])
self.Coverage.glyphs = glyphs
- self.Value = [otBase.ValueRecord(valueFormat) for g in glyphs]
+ self.Value = [otBase.ValueRecord(valueFormat) for _ in glyphs]
self.ValueCount = len(self.Value)
for i,values in enumerate(padded):
@@ -339,7 +339,7 @@ def _PairPosFormat1_merge(self, lst, merger):
default=empty)
self.Coverage.glyphs = glyphs
- self.PairSet = [ot.PairSet() for g in glyphs]
+ self.PairSet = [ot.PairSet() for _ in glyphs]
self.PairSetCount = len(self.PairSet)
for glyph, ps in zip(glyphs, self.PairSet):
ps._firstGlyph = glyph
diff --git a/Lib/fontTools/varLib/models.py b/Lib/fontTools/varLib/models.py
index d6837ee6..eddf3b21 100644
--- a/Lib/fontTools/varLib/models.py
+++ b/Lib/fontTools/varLib/models.py
@@ -422,26 +422,32 @@ def piecewiseLinearMap(v, mapping):
return va + (vb - va) * (v - a) / (b - a)
-def main(args):
+def main(args=None):
+ """Normalize locations on a given designspace"""
from fontTools import configLogger
+ import argparse
- args = args[1:]
+ parser = argparse.ArgumentParser(
+ "fonttools varLib.models",
+ description=main.__doc__,
+ )
+ parser.add_argument('--loglevel', metavar='LEVEL', default="INFO",
+ help="Logging level (defaults to INFO)")
- # TODO: allow user to configure logging via command-line options
- configLogger(level="INFO")
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument('-d', '--designspace',metavar="DESIGNSPACE",type=str)
+ group.add_argument('-l', '--locations', metavar='LOCATION', nargs='+',
+ help="Master locations as comma-separate coordinates. One must be all zeros.")
- if len(args) < 1:
- print("usage: fonttools varLib.models source.designspace", file=sys.stderr)
- print(" or")
- print("usage: fonttools varLib.models location1 location2 ...", file=sys.stderr)
- sys.exit(1)
+ args = parser.parse_args(args)
+ configLogger(level=args.loglevel)
from pprint import pprint
- if len(args) == 1 and args[0].endswith('.designspace'):
+ if args.designspacefile:
from fontTools.designspaceLib import DesignSpaceDocument
doc = DesignSpaceDocument()
- doc.read(args[0])
+ doc.read(args.designspacefile)
locs = [s.location for s in doc.sources]
print("Original locations:")
pprint(locs)
@@ -451,7 +457,7 @@ def main(args):
pprint(locs)
else:
axes = [chr(c) for c in range(ord('A'), ord('Z')+1)]
- locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args]
+ locs = [dict(zip(axes, (float(v) for v in s.split(',')))) for s in args.locations]
model = VariationModel(locs)
print("Sorted locations:")
@@ -463,6 +469,6 @@ if __name__ == "__main__":
import doctest, sys
if len(sys.argv) > 1:
- sys.exit(main(sys.argv))
+ sys.exit(main())
sys.exit(doctest.testmod().failed)
diff --git a/Lib/fontTools/varLib/mutator.py b/Lib/fontTools/varLib/mutator.py
index bd888249..b5954f8b 100644
--- a/Lib/fontTools/varLib/mutator.py
+++ b/Lib/fontTools/varLib/mutator.py
@@ -399,6 +399,7 @@ def instantiateVariableFont(varfont, location, inplace=False, overlap=True):
def main(args=None):
+ """Instantiate a variation font"""
from fontTools import configLogger
import argparse
diff --git a/Lib/fontTools/varLib/varStore.py b/Lib/fontTools/varLib/varStore.py
index 7239e954..3d9566a1 100644
--- a/Lib/fontTools/varLib/varStore.py
+++ b/Lib/fontTools/varLib/varStore.py
@@ -545,12 +545,13 @@ ot.VarStore.optimize = VarStore_optimize
def main(args=None):
+ """Optimize a font's GDEF variation store"""
from argparse import ArgumentParser
from fontTools import configLogger
from fontTools.ttLib import TTFont
from fontTools.ttLib.tables.otBase import OTTableWriter
- parser = ArgumentParser(prog='varLib.varStore')
+ parser = ArgumentParser(prog='varLib.varStore', description= main.__doc__)
parser.add_argument('fontfile')
parser.add_argument('outfile', nargs='?')
options = parser.parse_args(args)
diff --git a/Lib/fonttools.egg-info/PKG-INFO b/Lib/fonttools.egg-info/PKG-INFO
index af6d959d..3ca17207 100644
--- a/Lib/fonttools.egg-info/PKG-INFO
+++ b/Lib/fonttools.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.9.0
+Version: 4.10.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -21,6 +21,9 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
+ `User documentation <https://fonttools.readthedocs.io/en/latest/>` and
+ `developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
+
Installation
~~~~~~~~~~~~
@@ -64,112 +67,6 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
# install in 'editable' mode
pip install -e .
- TTX – From OpenType and TrueType to XML and Back
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Once installed you can use the ``ttx`` command to convert binary font
- files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
- convert them back to binary format. TTX files have a .ttx file
- extension.
-
- .. code:: sh
-
- ttx /path/to/font.otf
- ttx /path/to/font.ttx
-
- The TTX application can be used in two ways, depending on what
- platform you run it on:
-
- - As a command line tool (Windows/DOS, Unix, macOS)
- - By dropping files onto the application (Windows, macOS)
-
- TTX detects what kind of files it is fed: it will output a ``.ttx`` file
- when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
- ``.otf`` when the input file is a ``.ttx`` file. By default, the output
- file is created in the same folder as the input file, and will have the
- same name as the input file but with a different extension. TTX will
- *never* overwrite existing files, but if necessary will append a unique
- number to the output filename (before the extension) such as
- ``Arial#1.ttf``
-
- When using TTX from the command line there are a bunch of extra options.
- These are explained in the help text, as displayed when typing
- ``ttx -h`` at the command prompt. These additional options include:
-
- - specifying the folder where the output files are created
- - specifying which tables to dump or which tables to exclude
- - merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf``
- files
- - listing brief table info instead of dumping to ``.ttx``
- - splitting tables to separate ``.ttx`` files
- - disabling TrueType instruction disassembly
-
- The TTX file format
- -------------------
-
- The following tables are currently supported:
-
- .. begin table list
- .. code::
-
- BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
- Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH,
- MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1,
- TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX,
- VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm,
- fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar,
- loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop,
- sbix, trak, vhea and vmtx
- .. end table list
-
- Other tables are dumped as hexadecimal data.
-
- TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most
- places. While this is fine in binary form, it is really hard to work
- with for humans. Therefore we use names instead.
-
- The glyph names are either extracted from the ``CFF`` table or the
- ``post`` table, or are derived from a Unicode ``cmap`` table. In the
- latter case the Adobe Glyph List is used to calculate names based on
- Unicode values. If all of these methods fail, names are invented based
- on GlyphID (eg ``glyph00142``)
-
- It is possible that different glyphs use the same name. If this happens,
- we force the names to be unique by appending ``#n`` to the name (``n``
- being an integer number.) The original names are being kept, so this has
- no influence on a "round tripped" font.
-
- Because the order in which glyphs are stored inside the binary font is
- important, we maintain an ordered list of glyph names in the font.
-
- Other Tools
- ~~~~~~~~~~~
-
- Commands for merging and subsetting fonts are also available:
-
- .. code:: sh
-
- pyftmerge
- pyftsubset
-
- fontTools Python Module
- ~~~~~~~~~~~~~~~~~~~~~~~
-
- The fontTools Python module provides a convenient way to
- programmatically edit font files.
-
- .. code:: py
-
- >>> from fontTools.ttLib import TTFont
- >>> font = TTFont('/path/to/font.ttf')
- >>> font
- <fontTools.ttLib.TTFont object at 0x10c34ed50>
- >>>
-
- A selection of sample Python programs is in the
- `Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
- directory.
-
Optional Requirements
---------------------
@@ -307,64 +204,6 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
for generating PDFs and graphics.
- Testing
- ~~~~~~~
-
- To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
- When you run the ``pytest`` command, the tests will run against the
- installed ``fontTools`` package, or the first one found in the
- ``PYTHONPATH``.
-
- You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
- automatically run tests on different Python versions in isolated virtual
- environments.
-
- .. code:: sh
-
- pip install tox
- tox
-
- Note that when you run ``tox`` without arguments, the tests are executed
- for all the environments listed in tox.ini's ``envlist``. In our case,
- this includes Python 3.6 and 3.7, so for this to work the ``python3.6``
- and ``python3.7`` executables must be available in your ``PATH``.
-
- You can specify an alternative environment list via the ``-e`` option,
- or the ``TOXENV`` environment variable:
-
- .. code:: sh
-
- tox -e py36
- TOXENV="py36-cov,htmlcov" tox
-
- Development Community
- ~~~~~~~~~~~~~~~~~~~~~
-
- TTX/FontTools development is ongoing in an active community of
- developers, that includes professional developers employed at major
- software corporations and type foundries as well as hobbyists.
-
- Feature requests and bug reports are always welcome at
- https://github.com/fonttools/fonttools/issues/
-
- The best place for discussions about TTX from an end-user perspective as
- well as TTX/FontTools development is the
- https://groups.google.com/d/forum/fonttools mailing list. There is also
- a development https://groups.google.com/d/forum/fonttools-dev mailing
- list for continuous integration notifications. You can also email Behdad
- privately at behdad@behdad.org
-
- History
- ~~~~~~~
-
- The fontTools project was started by Just van Rossum in 1999, and was
- maintained as an open source project at
- http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
- began helping Just with stability maintenance. In 2013 Behdad Esfahbod
- began a friendly fork, thoroughly reviewing the codebase and making
- changes at https://github.com/behdad/fonttools to add new features and
- support for new font formats.
-
Acknowledgements
~~~~~~~~~~~~~~~~
@@ -414,6 +253,34 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.10.0 (released 2020-05-15)
+ ----------------------------
+
+ - [varLib] Allow feature variations to be active across the entire space (#1957).
+ - [ufoLib] Added support for ``formatVersionMinor`` in UFO's ``fontinfo.plist`` and for
+ ``formatMinor`` attribute in GLIF file as discussed in unified-font-object/ufo-spec#78.
+ No changes in reading or writing UFOs until an upcoming (non-0) minor update of the
+ UFO specification is published (#1786).
+ - [merge] Fixed merging fonts with different versions of ``OS/2`` table (#1865, #1952).
+ - [subset] Fixed ``AttributeError`` while subsetting ``ContextSubst`` and ``ContextPos``
+ Format 3 subtable (#1879, #1944).
+ - [ttLib.table._m_e_t_a] if data happens to be ascii, emit comment in TTX (#1938).
+ - [feaLib] Support multiple lookups per glyph position (#1905).
+ - [psCharStrings] Use inheritance to avoid repeated code in initializer (#1932).
+ - [Doc] Improved documentation for the following modules: ``afmLib`` (#1933), ``agl``
+ (#1934), ``cffLib`` (#1935), ``cu2qu`` (#1937), ``encodings`` (#1940), ``feaLib``
+ (#1941), ``merge`` (#1949).
+ - [Doc] Split off developer-centric info to new page, making front page of docs more
+ user-focused. List all utilities and sub-modules with brief descriptions.
+ Make README more concise and focused (#1914).
+ - [otlLib] Add function to build STAT table from high-level description (#1926).
+ - [ttLib._n_a_m_e] Add ``findMultilingualName()`` method (#1921).
+ - [unicodedata] Update ``RTL_SCRIPTS`` for Unicode 13.0 (#1925).
+ - [gvar] Sort ``gvar`` XML output by glyph name, not glyph order (#1907, #1908).
+ - [Doc] Added help options to ``fonttools`` command line tool (#1913, #1920).
+ Ensure all fonttools CLI tools have help documentation (#1948).
+ - [ufoLib] Only write fontinfo.plist when there actually is content (#1911).
+
4.9.0 (released 2020-04-29)
---------------------------
@@ -2079,12 +1946,12 @@ Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
Provides-Extra: interpolatable
-Provides-Extra: woff
+Provides-Extra: ufo
+Provides-Extra: graphite
Provides-Extra: lxml
-Provides-Extra: type1
Provides-Extra: unicode
-Provides-Extra: graphite
-Provides-Extra: all
-Provides-Extra: ufo
-Provides-Extra: symfont
+Provides-Extra: type1
Provides-Extra: plot
+Provides-Extra: woff
+Provides-Extra: symfont
+Provides-Extra: all
diff --git a/Lib/fonttools.egg-info/SOURCES.txt b/Lib/fonttools.egg-info/SOURCES.txt
index 28190040..2f999a4a 100644
--- a/Lib/fonttools.egg-info/SOURCES.txt
+++ b/Lib/fonttools.egg-info/SOURCES.txt
@@ -27,9 +27,11 @@ Doc/man/man1/ttx.1
Doc/source/afmLib.rst
Doc/source/agl.rst
Doc/source/conf.py
+Doc/source/developer.rst
Doc/source/index.rst
Doc/source/merge.rst
Doc/source/mtiLib.rst
+Doc/source/optional.rst
Doc/source/t1Lib.rst
Doc/source/ttx.rst
Doc/source/unicode.rst
@@ -38,27 +40,13 @@ Doc/source/assets/img/favicon.ico
Doc/source/cffLib/index.rst
Doc/source/cffLib/specializer.rst
Doc/source/cffLib/width.rst
-Doc/source/colorLib/builder.rst
-Doc/source/colorLib/errors.rst
Doc/source/colorLib/index.rst
-Doc/source/cu2qu/cli.rst
-Doc/source/cu2qu/cu2qu.rst
-Doc/source/cu2qu/errors.rst
Doc/source/cu2qu/index.rst
-Doc/source/cu2qu/ufo.rst
Doc/source/designspaceLib/index.rst
Doc/source/designspaceLib/readme.rst
Doc/source/designspaceLib/scripting.rst
-Doc/source/encodings/StandardEncoding.rst
-Doc/source/encodings/codecs.rst
Doc/source/encodings/index.rst
-Doc/source/encodings/macRoman.rst
-Doc/source/feaLib/ast.rst
-Doc/source/feaLib/builder.rst
-Doc/source/feaLib/error.rst
Doc/source/feaLib/index.rst
-Doc/source/feaLib/lexer.rst
-Doc/source/feaLib/parser.rst
Doc/source/misc/arrayTools.rst
Doc/source/misc/bezierTools.rst
Doc/source/misc/classifyTools.rst
@@ -166,6 +154,7 @@ Lib/fontTools/__main__.py
Lib/fontTools/afmLib.py
Lib/fontTools/agl.py
Lib/fontTools/fontBuilder.py
+Lib/fontTools/help.py
Lib/fontTools/merge.py
Lib/fontTools/ttx.py
Lib/fontTools/unicode.py
@@ -609,6 +598,10 @@ Tests/feaLib/data/LigatureCaretByPos.fea
Tests/feaLib/data/LigatureCaretByPos.ttx
Tests/feaLib/data/LigatureSubtable.fea
Tests/feaLib/data/LigatureSubtable.ttx
+Tests/feaLib/data/MultipleLookupsPerGlyph.fea
+Tests/feaLib/data/MultipleLookupsPerGlyph.ttx
+Tests/feaLib/data/MultipleLookupsPerGlyph2.fea
+Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx
Tests/feaLib/data/MultipleSubstSubtable.fea
Tests/feaLib/data/MultipleSubstSubtable.ttx
Tests/feaLib/data/PairPosSubtable.fea
@@ -896,6 +889,7 @@ Tests/subset/data/TestBSLN-2.ttx
Tests/subset/data/TestBSLN-3.ttx
Tests/subset/data/TestCID-Regular.ttx
Tests/subset/data/TestCLR-Regular.ttx
+Tests/subset/data/TestContextSubstFormat3.ttx
Tests/subset/data/TestGVAR.ttx
Tests/subset/data/TestHVVAR.ttx
Tests/subset/data/TestLCAR-0.ttx
@@ -1616,6 +1610,7 @@ Tests/ufoLib/__init__.py
Tests/ufoLib/filenames_test.py
Tests/ufoLib/glifLib_test.py
Tests/ufoLib/testSupport.py
+Tests/ufoLib/ufoLib_test.py
Tests/ufoLib/testdata/DemoFont.ufo/fontinfo.plist
Tests/ufoLib/testdata/DemoFont.ufo/lib.plist
Tests/ufoLib/testdata/DemoFont.ufo/metainfo.plist
@@ -1687,6 +1682,8 @@ Tests/varLib/data/BuildAvarIdentityMaps.designspace
Tests/varLib/data/BuildAvarSingleAxis.designspace
Tests/varLib/data/BuildGvarCompositeExplicitDelta.designspace
Tests/varLib/data/FeatureVars.designspace
+Tests/varLib/data/FeatureVarsWholeRange.designspace
+Tests/varLib/data/FeatureVarsWholeRangeEmpty.designspace
Tests/varLib/data/InterpolateLayout.designspace
Tests/varLib/data/InterpolateLayout2.designspace
Tests/varLib/data/InterpolateLayout3.designspace
@@ -2026,6 +2023,7 @@ Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx
Tests/varLib/data/test_results/BuildMain.ttx
Tests/varLib/data/test_results/BuildTestCFF2.ttx
Tests/varLib/data/test_results/FeatureVars.ttx
+Tests/varLib/data/test_results/FeatureVarsWholeRange.ttx
Tests/varLib/data/test_results/FeatureVars_rclt.ttx
Tests/varLib/data/test_results/InterpolateLayout.ttx
Tests/varLib/data/test_results/InterpolateLayout2.ttx
diff --git a/METADATA b/METADATA
index 8215e3bc..404fec6d 100644
--- a/METADATA
+++ b/METADATA
@@ -7,13 +7,13 @@ third_party {
}
url {
type: ARCHIVE
- value: "https://github.com/fonttools/fonttools/releases/download/4.9.0/fonttools-4.9.0.zip"
+ value: "https://github.com/fonttools/fonttools/releases/download/4.10.0/fonttools-4.10.0.zip"
}
- version: "4.9.0"
+ version: "4.10.0"
license_type: NOTICE
last_upgrade_date {
year: 2020
- month: 4
- day: 29
+ month: 5
+ day: 15
}
}
diff --git a/MetaTools/buildTableList.py b/MetaTools/buildTableList.py
index 825f0db1..36b9fa0a 100755
--- a/MetaTools/buildTableList.py
+++ b/MetaTools/buildTableList.py
@@ -11,7 +11,7 @@ fontToolsDir = os.path.dirname(os.path.dirname(os.path.join(os.getcwd(), sys.arg
fontToolsDir= os.path.normpath(fontToolsDir)
tablesDir = os.path.join(fontToolsDir,
"Lib", "fontTools", "ttLib", "tables")
-docFile = os.path.join(fontToolsDir, "README.rst")
+docFile = os.path.join(fontToolsDir, "Doc/source/ttx.rst")
names = glob.glob1(tablesDir, "*.py")
@@ -54,7 +54,7 @@ if __name__ == "__main__":
''')
-begin = ".. begin table list\n.. code::\n"
+begin = ".. begin table list\n"
end = ".. end table list"
with open(docFile) as f:
doc = f.read()
@@ -64,9 +64,10 @@ beginPos = beginPos + len(begin) + 1
endPos = doc.find(end)
lines = textwrap.wrap(", ".join(tables[:-1]) + " and " + tables[-1], 66)
+intro = "The following tables are currently supported::\n\n"
blockquote = "\n".join(" "*4 + line for line in lines) + "\n"
-doc = doc[:beginPos] + blockquote + doc[endPos:]
+doc = doc[:beginPos] + intro + blockquote + "\n" + doc[endPos:]
with open(docFile, "w") as f:
f.write(doc)
diff --git a/MetaTools/roundTrip.py b/MetaTools/roundTrip.py
index d02ec4a5..f9094ab0 100755
--- a/MetaTools/roundTrip.py
+++ b/MetaTools/roundTrip.py
@@ -31,9 +31,9 @@ def usage():
def roundTrip(ttFile1, options, report):
fn = os.path.basename(ttFile1)
- xmlFile1 = tempfile.mktemp(".%s.ttx1" % fn)
- ttFile2 = tempfile.mktemp(".%s" % fn)
- xmlFile2 = tempfile.mktemp(".%s.ttx2" % fn)
+ xmlFile1 = tempfile.mkstemp(".%s.ttx1" % fn)
+ ttFile2 = tempfile.mkstemp(".%s" % fn)
+ xmlFile2 = tempfile.mkstemp(".%s.ttx2" % fn)
try:
ttx.ttDump(ttFile1, xmlFile1, options)
diff --git a/NEWS.rst b/NEWS.rst
index 7c9e69c8..1afb7ad0 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,3 +1,31 @@
+4.10.0 (released 2020-05-15)
+----------------------------
+
+- [varLib] Allow feature variations to be active across the entire space (#1957).
+- [ufoLib] Added support for ``formatVersionMinor`` in UFO's ``fontinfo.plist`` and for
+ ``formatMinor`` attribute in GLIF file as discussed in unified-font-object/ufo-spec#78.
+ No changes in reading or writing UFOs until an upcoming (non-0) minor update of the
+ UFO specification is published (#1786).
+- [merge] Fixed merging fonts with different versions of ``OS/2`` table (#1865, #1952).
+- [subset] Fixed ``AttributeError`` while subsetting ``ContextSubst`` and ``ContextPos``
+ Format 3 subtable (#1879, #1944).
+- [ttLib.table._m_e_t_a] if data happens to be ascii, emit comment in TTX (#1938).
+- [feaLib] Support multiple lookups per glyph position (#1905).
+- [psCharStrings] Use inheritance to avoid repeated code in initializer (#1932).
+- [Doc] Improved documentation for the following modules: ``afmLib`` (#1933), ``agl``
+ (#1934), ``cffLib`` (#1935), ``cu2qu`` (#1937), ``encodings`` (#1940), ``feaLib``
+ (#1941), ``merge`` (#1949).
+- [Doc] Split off developer-centric info to new page, making front page of docs more
+ user-focused. List all utilities and sub-modules with brief descriptions.
+ Make README more concise and focused (#1914).
+- [otlLib] Add function to build STAT table from high-level description (#1926).
+- [ttLib._n_a_m_e] Add ``findMultilingualName()`` method (#1921).
+- [unicodedata] Update ``RTL_SCRIPTS`` for Unicode 13.0 (#1925).
+- [gvar] Sort ``gvar`` XML output by glyph name, not glyph order (#1907, #1908).
+- [Doc] Added help options to ``fonttools`` command line tool (#1913, #1920).
+ Ensure all fonttools CLI tools have help documentation (#1948).
+- [ufoLib] Only write fontinfo.plist when there actually is content (#1911).
+
4.9.0 (released 2020-04-29)
---------------------------
diff --git a/PKG-INFO b/PKG-INFO
index af6d959d..3ca17207 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: fonttools
-Version: 4.9.0
+Version: 4.10.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -21,6 +21,9 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
+ `User documentation <https://fonttools.readthedocs.io/en/latest/>` and
+ `developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
+
Installation
~~~~~~~~~~~~
@@ -64,112 +67,6 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
# install in 'editable' mode
pip install -e .
- TTX – From OpenType and TrueType to XML and Back
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
- Once installed you can use the ``ttx`` command to convert binary font
- files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
- convert them back to binary format. TTX files have a .ttx file
- extension.
-
- .. code:: sh
-
- ttx /path/to/font.otf
- ttx /path/to/font.ttx
-
- The TTX application can be used in two ways, depending on what
- platform you run it on:
-
- - As a command line tool (Windows/DOS, Unix, macOS)
- - By dropping files onto the application (Windows, macOS)
-
- TTX detects what kind of files it is fed: it will output a ``.ttx`` file
- when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
- ``.otf`` when the input file is a ``.ttx`` file. By default, the output
- file is created in the same folder as the input file, and will have the
- same name as the input file but with a different extension. TTX will
- *never* overwrite existing files, but if necessary will append a unique
- number to the output filename (before the extension) such as
- ``Arial#1.ttf``
-
- When using TTX from the command line there are a bunch of extra options.
- These are explained in the help text, as displayed when typing
- ``ttx -h`` at the command prompt. These additional options include:
-
- - specifying the folder where the output files are created
- - specifying which tables to dump or which tables to exclude
- - merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf``
- files
- - listing brief table info instead of dumping to ``.ttx``
- - splitting tables to separate ``.ttx`` files
- - disabling TrueType instruction disassembly
-
- The TTX file format
- -------------------
-
- The following tables are currently supported:
-
- .. begin table list
- .. code::
-
- BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
- Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH,
- MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1,
- TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX,
- VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm,
- fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar,
- loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop,
- sbix, trak, vhea and vmtx
- .. end table list
-
- Other tables are dumped as hexadecimal data.
-
- TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most
- places. While this is fine in binary form, it is really hard to work
- with for humans. Therefore we use names instead.
-
- The glyph names are either extracted from the ``CFF`` table or the
- ``post`` table, or are derived from a Unicode ``cmap`` table. In the
- latter case the Adobe Glyph List is used to calculate names based on
- Unicode values. If all of these methods fail, names are invented based
- on GlyphID (eg ``glyph00142``)
-
- It is possible that different glyphs use the same name. If this happens,
- we force the names to be unique by appending ``#n`` to the name (``n``
- being an integer number.) The original names are being kept, so this has
- no influence on a "round tripped" font.
-
- Because the order in which glyphs are stored inside the binary font is
- important, we maintain an ordered list of glyph names in the font.
-
- Other Tools
- ~~~~~~~~~~~
-
- Commands for merging and subsetting fonts are also available:
-
- .. code:: sh
-
- pyftmerge
- pyftsubset
-
- fontTools Python Module
- ~~~~~~~~~~~~~~~~~~~~~~~
-
- The fontTools Python module provides a convenient way to
- programmatically edit font files.
-
- .. code:: py
-
- >>> from fontTools.ttLib import TTFont
- >>> font = TTFont('/path/to/font.ttf')
- >>> font
- <fontTools.ttLib.TTFont object at 0x10c34ed50>
- >>>
-
- A selection of sample Python programs is in the
- `Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
- directory.
-
Optional Requirements
---------------------
@@ -307,64 +204,6 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
for generating PDFs and graphics.
- Testing
- ~~~~~~~
-
- To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
- When you run the ``pytest`` command, the tests will run against the
- installed ``fontTools`` package, or the first one found in the
- ``PYTHONPATH``.
-
- You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
- automatically run tests on different Python versions in isolated virtual
- environments.
-
- .. code:: sh
-
- pip install tox
- tox
-
- Note that when you run ``tox`` without arguments, the tests are executed
- for all the environments listed in tox.ini's ``envlist``. In our case,
- this includes Python 3.6 and 3.7, so for this to work the ``python3.6``
- and ``python3.7`` executables must be available in your ``PATH``.
-
- You can specify an alternative environment list via the ``-e`` option,
- or the ``TOXENV`` environment variable:
-
- .. code:: sh
-
- tox -e py36
- TOXENV="py36-cov,htmlcov" tox
-
- Development Community
- ~~~~~~~~~~~~~~~~~~~~~
-
- TTX/FontTools development is ongoing in an active community of
- developers, that includes professional developers employed at major
- software corporations and type foundries as well as hobbyists.
-
- Feature requests and bug reports are always welcome at
- https://github.com/fonttools/fonttools/issues/
-
- The best place for discussions about TTX from an end-user perspective as
- well as TTX/FontTools development is the
- https://groups.google.com/d/forum/fonttools mailing list. There is also
- a development https://groups.google.com/d/forum/fonttools-dev mailing
- list for continuous integration notifications. You can also email Behdad
- privately at behdad@behdad.org
-
- History
- ~~~~~~~
-
- The fontTools project was started by Just van Rossum in 1999, and was
- maintained as an open source project at
- http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
- began helping Just with stability maintenance. In 2013 Behdad Esfahbod
- began a friendly fork, thoroughly reviewing the codebase and making
- changes at https://github.com/behdad/fonttools to add new features and
- support for new font formats.
-
Acknowledgements
~~~~~~~~~~~~~~~~
@@ -414,6 +253,34 @@ Description: |Travis Build Status| |Appveyor Build status| |Coverage Status| |Py
Changelog
~~~~~~~~~
+ 4.10.0 (released 2020-05-15)
+ ----------------------------
+
+ - [varLib] Allow feature variations to be active across the entire space (#1957).
+ - [ufoLib] Added support for ``formatVersionMinor`` in UFO's ``fontinfo.plist`` and for
+ ``formatMinor`` attribute in GLIF file as discussed in unified-font-object/ufo-spec#78.
+ No changes in reading or writing UFOs until an upcoming (non-0) minor update of the
+ UFO specification is published (#1786).
+ - [merge] Fixed merging fonts with different versions of ``OS/2`` table (#1865, #1952).
+ - [subset] Fixed ``AttributeError`` while subsetting ``ContextSubst`` and ``ContextPos``
+ Format 3 subtable (#1879, #1944).
+ - [ttLib.table._m_e_t_a] if data happens to be ascii, emit comment in TTX (#1938).
+ - [feaLib] Support multiple lookups per glyph position (#1905).
+ - [psCharStrings] Use inheritance to avoid repeated code in initializer (#1932).
+ - [Doc] Improved documentation for the following modules: ``afmLib`` (#1933), ``agl``
+ (#1934), ``cffLib`` (#1935), ``cu2qu`` (#1937), ``encodings`` (#1940), ``feaLib``
+ (#1941), ``merge`` (#1949).
+ - [Doc] Split off developer-centric info to new page, making front page of docs more
+ user-focused. List all utilities and sub-modules with brief descriptions.
+ Make README more concise and focused (#1914).
+ - [otlLib] Add function to build STAT table from high-level description (#1926).
+ - [ttLib._n_a_m_e] Add ``findMultilingualName()`` method (#1921).
+ - [unicodedata] Update ``RTL_SCRIPTS`` for Unicode 13.0 (#1925).
+ - [gvar] Sort ``gvar`` XML output by glyph name, not glyph order (#1907, #1908).
+ - [Doc] Added help options to ``fonttools`` command line tool (#1913, #1920).
+ Ensure all fonttools CLI tools have help documentation (#1948).
+ - [ufoLib] Only write fontinfo.plist when there actually is content (#1911).
+
4.9.0 (released 2020-04-29)
---------------------------
@@ -2079,12 +1946,12 @@ Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
Requires-Python: >=3.6
Provides-Extra: interpolatable
-Provides-Extra: woff
+Provides-Extra: ufo
+Provides-Extra: graphite
Provides-Extra: lxml
-Provides-Extra: type1
Provides-Extra: unicode
-Provides-Extra: graphite
-Provides-Extra: all
-Provides-Extra: ufo
-Provides-Extra: symfont
+Provides-Extra: type1
Provides-Extra: plot
+Provides-Extra: woff
+Provides-Extra: symfont
+Provides-Extra: all
diff --git a/README.rst b/README.rst
index dc961c2c..2fade22e 100644
--- a/README.rst
+++ b/README.rst
@@ -11,6 +11,9 @@ What is this?
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
+`User documentation <https://fonttools.readthedocs.io/en/latest/>` and
+`developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
+
Installation
~~~~~~~~~~~~
@@ -54,112 +57,6 @@ Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module.
# install in 'editable' mode
pip install -e .
-TTX – From OpenType and TrueType to XML and Back
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Once installed you can use the ``ttx`` command to convert binary font
-files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
-convert them back to binary format. TTX files have a .ttx file
-extension.
-
-.. code:: sh
-
- ttx /path/to/font.otf
- ttx /path/to/font.ttx
-
-The TTX application can be used in two ways, depending on what
-platform you run it on:
-
-- As a command line tool (Windows/DOS, Unix, macOS)
-- By dropping files onto the application (Windows, macOS)
-
-TTX detects what kind of files it is fed: it will output a ``.ttx`` file
-when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
-``.otf`` when the input file is a ``.ttx`` file. By default, the output
-file is created in the same folder as the input file, and will have the
-same name as the input file but with a different extension. TTX will
-*never* overwrite existing files, but if necessary will append a unique
-number to the output filename (before the extension) such as
-``Arial#1.ttf``
-
-When using TTX from the command line there are a bunch of extra options.
-These are explained in the help text, as displayed when typing
-``ttx -h`` at the command prompt. These additional options include:
-
-- specifying the folder where the output files are created
-- specifying which tables to dump or which tables to exclude
-- merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf``
- files
-- listing brief table info instead of dumping to ``.ttx``
-- splitting tables to separate ``.ttx`` files
-- disabling TrueType instruction disassembly
-
-The TTX file format
--------------------
-
-The following tables are currently supported:
-
-.. begin table list
-.. code::
-
- BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
- Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH,
- MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1,
- TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX,
- VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm,
- fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar,
- loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop,
- sbix, trak, vhea and vmtx
-.. end table list
-
-Other tables are dumped as hexadecimal data.
-
-TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most
-places. While this is fine in binary form, it is really hard to work
-with for humans. Therefore we use names instead.
-
-The glyph names are either extracted from the ``CFF`` table or the
-``post`` table, or are derived from a Unicode ``cmap`` table. In the
-latter case the Adobe Glyph List is used to calculate names based on
-Unicode values. If all of these methods fail, names are invented based
-on GlyphID (eg ``glyph00142``)
-
-It is possible that different glyphs use the same name. If this happens,
-we force the names to be unique by appending ``#n`` to the name (``n``
-being an integer number.) The original names are being kept, so this has
-no influence on a "round tripped" font.
-
-Because the order in which glyphs are stored inside the binary font is
-important, we maintain an ordered list of glyph names in the font.
-
-Other Tools
-~~~~~~~~~~~
-
-Commands for merging and subsetting fonts are also available:
-
-.. code:: sh
-
- pyftmerge
- pyftsubset
-
-fontTools Python Module
-~~~~~~~~~~~~~~~~~~~~~~~
-
-The fontTools Python module provides a convenient way to
-programmatically edit font files.
-
-.. code:: py
-
- >>> from fontTools.ttLib import TTFont
- >>> font = TTFont('/path/to/font.ttf')
- >>> font
- <fontTools.ttLib.TTFont object at 0x10c34ed50>
- >>>
-
-A selection of sample Python programs is in the
-`Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
-directory.
-
Optional Requirements
---------------------
@@ -297,64 +194,6 @@ are required to unlock the extra features named "ufo", etc.
* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
for generating PDFs and graphics.
-Testing
-~~~~~~~
-
-To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
-When you run the ``pytest`` command, the tests will run against the
-installed ``fontTools`` package, or the first one found in the
-``PYTHONPATH``.
-
-You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
-automatically run tests on different Python versions in isolated virtual
-environments.
-
-.. code:: sh
-
- pip install tox
- tox
-
-Note that when you run ``tox`` without arguments, the tests are executed
-for all the environments listed in tox.ini's ``envlist``. In our case,
-this includes Python 3.6 and 3.7, so for this to work the ``python3.6``
-and ``python3.7`` executables must be available in your ``PATH``.
-
-You can specify an alternative environment list via the ``-e`` option,
-or the ``TOXENV`` environment variable:
-
-.. code:: sh
-
- tox -e py36
- TOXENV="py36-cov,htmlcov" tox
-
-Development Community
-~~~~~~~~~~~~~~~~~~~~~
-
-TTX/FontTools development is ongoing in an active community of
-developers, that includes professional developers employed at major
-software corporations and type foundries as well as hobbyists.
-
-Feature requests and bug reports are always welcome at
-https://github.com/fonttools/fonttools/issues/
-
-The best place for discussions about TTX from an end-user perspective as
-well as TTX/FontTools development is the
-https://groups.google.com/d/forum/fonttools mailing list. There is also
-a development https://groups.google.com/d/forum/fonttools-dev mailing
-list for continuous integration notifications. You can also email Behdad
-privately at behdad@behdad.org
-
-History
-~~~~~~~
-
-The fontTools project was started by Just van Rossum in 1999, and was
-maintained as an open source project at
-http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
-began helping Just with stability maintenance. In 2013 Behdad Esfahbod
-began a friendly fork, thoroughly reviewing the codebase and making
-changes at https://github.com/behdad/fonttools to add new features and
-support for new font formats.
-
Acknowledgements
~~~~~~~~~~~~~~~~
diff --git a/Tests/designspaceLib/designspace_test.py b/Tests/designspaceLib/designspace_test.py
index 378effe6..f883730f 100644
--- a/Tests/designspaceLib/designspace_test.py
+++ b/Tests/designspaceLib/designspace_test.py
@@ -996,23 +996,30 @@ def test_addInstanceDescriptor():
assert instance.styleMapStyleName == "regular"
-def test_addRuleDescriptor():
+def test_addRuleDescriptor(tmp_path):
ds = DesignSpaceDocument()
rule = ds.addRuleDescriptor(
- name="TestRule",
- conditionSets=[
- dict(name='Weight', minimum=100, maximum=200),
- dict(name='Weight', minimum=700, maximum=900),
- ],
- subs=[("a", "a.alt")],
+ name="TestRule",
+ conditionSets=[
+ [
+ dict(name="Weight", minimum=100, maximum=200),
+ dict(name="Weight", minimum=700, maximum=900),
+ ]
+ ],
+ subs=[("a", "a.alt")],
)
assert ds.rules[0] is rule
assert isinstance(rule, RuleDescriptor)
assert rule.name == "TestRule"
assert rule.conditionSets == [
- dict(name='Weight', minimum=100, maximum=200),
- dict(name='Weight', minimum=700, maximum=900),
+ [
+ dict(name="Weight", minimum=100, maximum=200),
+ dict(name="Weight", minimum=700, maximum=900),
+ ]
]
assert rule.subs == [("a", "a.alt")]
+
+ # Test it doesn't crash.
+ ds.write(tmp_path / "test.designspace")
diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py
index 5ce4cc26..f2f1c05d 100644
--- a/Tests/feaLib/builder_test.py
+++ b/Tests/feaLib/builder_test.py
@@ -71,7 +71,8 @@ class BuilderTest(unittest.TestCase):
ZeroValue_ChainSinglePos_horizontal ZeroValue_ChainSinglePos_vertical
PairPosSubtable ChainSubstSubtable ChainPosSubtable LigatureSubtable
AlternateSubtable MultipleSubstSubtable SingleSubstSubtable
- aalt_chain_contextual_subst AlternateChained
+ aalt_chain_contextual_subst AlternateChained MultipleLookupsPerGlyph
+ MultipleLookupsPerGlyph2
""".split()
def __init__(self, methodName):
diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph.fea b/Tests/feaLib/data/MultipleLookupsPerGlyph.fea
new file mode 100644
index 00000000..e0c22226
--- /dev/null
+++ b/Tests/feaLib/data/MultipleLookupsPerGlyph.fea
@@ -0,0 +1,11 @@
+lookup a_to_bc {
+ sub a by b c;
+} a_to_bc;
+
+lookup b_to_d {
+ sub b by d;
+} b_to_d;
+
+feature test {
+ sub a' lookup a_to_bc lookup b_to_d b;
+} test; \ No newline at end of file
diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx b/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx
new file mode 100644
index 00000000..927694cb
--- /dev/null
+++ b/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <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="test"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="2"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=3 -->
+ <Lookup index="0">
+ <LookupType value="2"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <MultipleSubst index="0">
+ <Substitution in="a" out="b,c"/>
+ </MultipleSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0">
+ <Substitution in="b" out="d"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="6"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <ChainContextSubst index="0" Format="3">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=1 -->
+ <InputCoverage index="0">
+ <Glyph value="a"/>
+ </InputCoverage>
+ <!-- LookAheadGlyphCount=1 -->
+ <LookAheadCoverage index="0">
+ <Glyph value="b"/>
+ </LookAheadCoverage>
+ <!-- SubstCount=2 -->
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="0"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="1">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="1"/>
+ </SubstLookupRecord>
+ </ChainContextSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea b/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea
new file mode 100644
index 00000000..5a9d19b2
--- /dev/null
+++ b/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea
@@ -0,0 +1,11 @@
+lookup a_reduce_sb {
+ pos a <-80 0 -160 0>;
+} a_reduce_sb;
+
+lookup a_raise {
+ pos a <0 100 0 0>;
+} a_raise;
+
+feature test {
+ pos a' lookup a_reduce_sb lookup a_raise b;
+} test; \ No newline at end of file
diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx b/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx
new file mode 100644
index 00000000..008d95b6
--- /dev/null
+++ b/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <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="test"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="2"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=3 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SinglePos index="0" Format="1">
+ <Coverage>
+ <Glyph value="a"/>
+ </Coverage>
+ <ValueFormat value="5"/>
+ <Value XPlacement="-80" XAdvance="-160"/>
+ </SinglePos>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SinglePos index="0" Format="1">
+ <Coverage>
+ <Glyph value="a"/>
+ </Coverage>
+ <ValueFormat value="2"/>
+ <Value YPlacement="100"/>
+ </SinglePos>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="8"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <ChainContextPos index="0" Format="3">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=1 -->
+ <InputCoverage index="0">
+ <Glyph value="a"/>
+ </InputCoverage>
+ <!-- LookAheadGlyphCount=1 -->
+ <LookAheadCoverage index="0">
+ <Glyph value="b"/>
+ </LookAheadCoverage>
+ <!-- PosCount=2 -->
+ <PosLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="0"/>
+ </PosLookupRecord>
+ <PosLookupRecord index="1">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="1"/>
+ </PosLookupRecord>
+ </ChainContextPos>
+ </Lookup>
+ </LookupList>
+ </GPOS>
+
+</ttFont>
diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py
index d05a8244..87b8c96a 100644
--- a/Tests/feaLib/parser_test.py
+++ b/Tests/feaLib/parser_test.py
@@ -1065,7 +1065,7 @@ class ParserTest(unittest.TestCase):
self.assertEqual(glyphstr(pos.prefix), "[A a] [B b]")
self.assertEqual(glyphstr(pos.glyphs), "I [N n] P")
self.assertEqual(glyphstr(pos.suffix), "[Y y] [Z z]")
- self.assertEqual(pos.lookups, [lookup1, lookup2, None])
+ self.assertEqual(pos.lookups, [[lookup1], [lookup2], None])
def test_gpos_type_8_lookup_with_values(self):
self.assertRaisesRegex(
@@ -1508,8 +1508,8 @@ class ParserTest(unittest.TestCase):
def test_substitute_lookups(self): # GSUB LookupType 6
doc = Parser(self.getpath("spec5fi1.fea"), GLYPHNAMES).parse()
[_, _, _, langsys, ligs, sub, feature] = doc.statements
- self.assertEqual(feature.statements[0].lookups, [ligs, None, sub])
- self.assertEqual(feature.statements[1].lookups, [ligs, None, sub])
+ self.assertEqual(feature.statements[0].lookups, [[ligs], None, [sub]])
+ self.assertEqual(feature.statements[1].lookups, [[ligs], None, [sub]])
def test_substitute_missing_by(self):
self.assertRaisesRegex(
diff --git a/Tests/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx
index bc1aae25..ed8fd307 100644
--- a/Tests/fontBuilder/data/test_var.ttf.ttx
+++ b/Tests/fontBuilder/data/test_var.ttf.ttx
@@ -204,6 +204,9 @@
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Right Up
</namerecord>
+ <namerecord nameID="262" platformID="1" platEncID="0" langID="0x0" unicode="True">
+ Neutral
+ </namerecord>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
HalloTestFont
</namerecord>
@@ -237,6 +240,9 @@
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Right Up
</namerecord>
+ <namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
+ Neutral
+ </namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
HalloTestFont
</namerecord>
@@ -363,6 +369,86 @@
</FeatureVariations>
</GSUB>
+ <STAT>
+ <Version value="0x00010001"/>
+ <DesignAxisRecordSize value="8"/>
+ <!-- DesignAxisCount=4 -->
+ <DesignAxisRecord>
+ <Axis index="0">
+ <AxisTag value="LEFT"/>
+ <AxisNameID value="256"/> <!-- Left -->
+ <AxisOrdering value="0"/>
+ </Axis>
+ <Axis index="1">
+ <AxisTag value="RGHT"/>
+ <AxisNameID value="257"/> <!-- Right -->
+ <AxisOrdering value="1"/>
+ </Axis>
+ <Axis index="2">
+ <AxisTag value="UPPP"/>
+ <AxisNameID value="258"/> <!-- Up -->
+ <AxisOrdering value="2"/>
+ </Axis>
+ <Axis index="3">
+ <AxisTag value="DOWN"/>
+ <AxisNameID value="259"/> <!-- Down -->
+ <AxisOrdering value="3"/>
+ </Axis>
+ </DesignAxisRecord>
+ <!-- AxisValueCount=8 -->
+ <AxisValueArray>
+ <AxisValue index="0" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="2"/>
+ <ValueNameID value="262"/> <!-- Neutral -->
+ <Value value="0.0"/>
+ </AxisValue>
+ <AxisValue index="1" Format="1">
+ <AxisIndex value="0"/>
+ <Flags value="0"/>
+ <ValueNameID value="256"/> <!-- Left -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="2" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="2"/>
+ <ValueNameID value="262"/> <!-- Neutral -->
+ <Value value="0.0"/>
+ </AxisValue>
+ <AxisValue index="3" Format="1">
+ <AxisIndex value="1"/>
+ <Flags value="0"/>
+ <ValueNameID value="257"/> <!-- Right -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="4" Format="1">
+ <AxisIndex value="2"/>
+ <Flags value="2"/>
+ <ValueNameID value="262"/> <!-- Neutral -->
+ <Value value="0.0"/>
+ </AxisValue>
+ <AxisValue index="5" Format="1">
+ <AxisIndex value="2"/>
+ <Flags value="0"/>
+ <ValueNameID value="258"/> <!-- Up -->
+ <Value value="100.0"/>
+ </AxisValue>
+ <AxisValue index="6" Format="1">
+ <AxisIndex value="3"/>
+ <Flags value="2"/>
+ <ValueNameID value="262"/> <!-- Neutral -->
+ <Value value="0.0"/>
+ </AxisValue>
+ <AxisValue index="7" Format="1">
+ <AxisIndex value="3"/>
+ <Flags value="0"/>
+ <ValueNameID value="259"/> <!-- Down -->
+ <Value value="100.0"/>
+ </AxisValue>
+ </AxisValueArray>
+ <ElidedFallbackNameID value="2"/> <!-- TotallyNormal -->
+ </STAT>
+
<fvar>
<!-- Left -->
diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py
index bc7837c3..6368cb87 100644
--- a/Tests/fontBuilder/fontBuilder_test.py
+++ b/Tests/fontBuilder/fontBuilder_test.py
@@ -226,6 +226,13 @@ def test_build_var(tmpdir):
featureTag="rclt",
)
+ statAxes = []
+ for tag, minVal, defaultVal, maxVal, name in axes:
+ values = [dict(name="Neutral", value=defaultVal, flags=0x2),
+ dict(name=name, value=maxVal)]
+ statAxes.append(dict(tag=tag, name=name, values=values))
+ fb.setupStat(statAxes)
+
fb.setupOS2()
fb.setupPost()
fb.setupDummyDSIG()
diff --git a/Tests/merge_test.py b/Tests/merge_test.py
index 00e719b8..0fb89c10 100644
--- a/Tests/merge_test.py
+++ b/Tests/merge_test.py
@@ -1,7 +1,12 @@
+import io
+import itertools
from fontTools.misc.py23 import *
from fontTools import ttLib
+from fontTools.ttLib.tables._g_l_y_f import Glyph
+from fontTools.fontBuilder import FontBuilder
from fontTools.merge import *
import unittest
+import pytest
class MergeIntegrationTest(unittest.TestCase):
@@ -113,6 +118,53 @@ class CmapMergeUnitTest(unittest.TestCase):
self.assertEqual(self.merger.duplicateGlyphsPerFont, [{}, {'space#0': 'space#1'}])
+def _compile(ttFont):
+ buf = io.BytesIO()
+ ttFont.save(buf)
+ buf.seek(0)
+ return buf
+
+
+def _make_fontfile_with_OS2(*, version, **kwargs):
+ upem = 1000
+ glyphOrder = [".notdef", "a"]
+ cmap = {0x61: "a"}
+ glyphs = {gn: Glyph() for gn in glyphOrder}
+ hmtx = {gn: (500, 0) for gn in glyphOrder}
+ names = {"familyName": "TestOS2", "styleName": "Regular"}
+
+ fb = FontBuilder(unitsPerEm=upem)
+ fb.setupGlyphOrder(glyphOrder)
+ fb.setupCharacterMap(cmap)
+ fb.setupGlyf(glyphs)
+ fb.setupHorizontalMetrics(hmtx)
+ fb.setupHorizontalHeader()
+ fb.setupNameTable(names)
+ fb.setupOS2(version=version, **kwargs)
+
+ return _compile(fb.font)
+
+
+def _merge_and_recompile(fontfiles, options=None):
+ merger = Merger(options)
+ merged = merger.merge(fontfiles)
+ buf = _compile(merged)
+ return ttLib.TTFont(buf)
+
+
+@pytest.mark.parametrize(
+ "v1, v2", list(itertools.permutations(range(5+1), 2))
+)
+def test_merge_OS2_mixed_versions(v1, v2):
+ # https://github.com/fonttools/fonttools/issues/1865
+ fontfiles = [
+ _make_fontfile_with_OS2(version=v1),
+ _make_fontfile_with_OS2(version=v2),
+ ]
+ merged = _merge_and_recompile(fontfiles)
+ assert merged["OS/2"].version == max(v1, v2)
+
+
if __name__ == "__main__":
import sys
sys.exit(unittest.main())
diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py
index 3675395f..727d685f 100644
--- a/Tests/otlLib/builder_test.py
+++ b/Tests/otlLib/builder_test.py
@@ -1,5 +1,9 @@
+import io
+import struct
+from fontTools.misc.fixedTools import floatToFixed
from fontTools.misc.testTools import getXML
from fontTools.otlLib import builder
+from fontTools import ttLib
from fontTools.ttLib.tables import otTables
import pytest
@@ -1106,6 +1110,291 @@ class ClassDefBuilderTest(object):
assert not b.canAdd({"f"})
+buildStatTable_test_data = [
+ ([
+ dict(
+ tag="wght",
+ name="Weight",
+ values=[
+ dict(value=100, name='Thin'),
+ dict(value=400, name='Regular', flags=0x2),
+ dict(value=900, name='Black')])], None, "Regular", [
+ ' <STAT>',
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ ' <!-- DesignAxisCount=1 -->',
+ ' <DesignAxisRecord>',
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="257"/> <!-- Weight -->',
+ ' <AxisOrdering value="0"/>',
+ ' </Axis>',
+ ' </DesignAxisRecord>',
+ ' <!-- AxisValueCount=3 -->',
+ ' <AxisValueArray>',
+ ' <AxisValue index="0" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="258"/> <!-- Thin -->',
+ ' <Value value="100.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="1" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/>',
+ ' <ValueNameID value="256"/> <!-- Regular -->',
+ ' <Value value="400.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="2" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Black -->',
+ ' <Value value="900.0"/>',
+ ' </AxisValue>',
+ ' </AxisValueArray>',
+ ' <ElidedFallbackNameID value="256"/> <!-- Regular -->',
+ ' </STAT>']),
+ ([
+ dict(
+ tag="wght",
+ name=dict(en="Weight", nl="Gewicht"),
+ values=[
+ dict(value=100, name=dict(en='Thin', nl='Dun')),
+ dict(value=400, name='Regular', flags=0x2),
+ dict(value=900, name='Black'),
+ ]),
+ dict(
+ tag="wdth",
+ name="Width",
+ values=[
+ dict(value=50, name='Condensed'),
+ dict(value=100, name='Regular', flags=0x2),
+ dict(value=200, name='Extended')])], None, 2, [
+ ' <STAT>',
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ ' <!-- DesignAxisCount=2 -->',
+ ' <DesignAxisRecord>',
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="256"/> <!-- Weight -->',
+ ' <AxisOrdering value="0"/>',
+ ' </Axis>',
+ ' <Axis index="1">',
+ ' <AxisTag value="wdth"/>',
+ ' <AxisNameID value="260"/> <!-- Width -->',
+ ' <AxisOrdering value="1"/>',
+ ' </Axis>',
+ ' </DesignAxisRecord>',
+ ' <!-- AxisValueCount=6 -->',
+ ' <AxisValueArray>',
+ ' <AxisValue index="0" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="257"/> <!-- Thin -->',
+ ' <Value value="100.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="1" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/>',
+ ' <ValueNameID value="258"/> <!-- Regular -->',
+ ' <Value value="400.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="2" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Black -->',
+ ' <Value value="900.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="3" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="261"/> <!-- Condensed -->',
+ ' <Value value="50.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="4" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="2"/>',
+ ' <ValueNameID value="258"/> <!-- Regular -->',
+ ' <Value value="100.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="5" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="262"/> <!-- Extended -->',
+ ' <Value value="200.0"/>',
+ ' </AxisValue>',
+ ' </AxisValueArray>',
+ ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
+ ' </STAT>']),
+ ([
+ dict(
+ tag="wght",
+ name="Weight",
+ values=[
+ dict(value=400, name='Regular', flags=0x2),
+ dict(value=600, linkedValue=650, name='Bold')])], None, 18, [
+ ' <STAT>',
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ ' <!-- DesignAxisCount=1 -->',
+ ' <DesignAxisRecord>',
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="256"/> <!-- Weight -->',
+ ' <AxisOrdering value="0"/>',
+ ' </Axis>',
+ ' </DesignAxisRecord>',
+ ' <!-- AxisValueCount=2 -->',
+ ' <AxisValueArray>',
+ ' <AxisValue index="0" Format="1">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/>',
+ ' <ValueNameID value="257"/> <!-- Regular -->',
+ ' <Value value="400.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="1" Format="3">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="258"/> <!-- Bold -->',
+ ' <Value value="600.0"/>',
+ ' <LinkedValue value="650.0"/>',
+ ' </AxisValue>',
+ ' </AxisValueArray>',
+ ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
+ ' </STAT>']),
+ ([
+ dict(
+ tag="opsz",
+ name="Optical Size",
+ values=[
+ dict(nominalValue=6, rangeMaxValue=10, name='Small'),
+ dict(rangeMinValue=10, nominalValue=14, rangeMaxValue=24, name='Text', flags=0x2),
+ dict(rangeMinValue=24, nominalValue=600, name='Display')])], None, 2, [
+ ' <STAT>',
+ ' <Version value="0x00010001"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ ' <!-- DesignAxisCount=1 -->',
+ ' <DesignAxisRecord>',
+ ' <Axis index="0">',
+ ' <AxisTag value="opsz"/>',
+ ' <AxisNameID value="256"/> <!-- Optical Size -->',
+ ' <AxisOrdering value="0"/>',
+ ' </Axis>',
+ ' </DesignAxisRecord>',
+ ' <!-- AxisValueCount=3 -->',
+ ' <AxisValueArray>',
+ ' <AxisValue index="0" Format="2">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="257"/> <!-- Small -->',
+ ' <NominalValue value="6.0"/>',
+ ' <RangeMinValue value="-32768.0"/>',
+ ' <RangeMaxValue value="10.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="1" Format="2">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="2"/>',
+ ' <ValueNameID value="258"/> <!-- Text -->',
+ ' <NominalValue value="14.0"/>',
+ ' <RangeMinValue value="10.0"/>',
+ ' <RangeMaxValue value="24.0"/>',
+ ' </AxisValue>',
+ ' <AxisValue index="2" Format="2">',
+ ' <AxisIndex value="0"/>',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Display -->',
+ ' <NominalValue value="600.0"/>',
+ ' <RangeMinValue value="24.0"/>',
+ ' <RangeMaxValue value="32767.99998"/>',
+ ' </AxisValue>',
+ ' </AxisValueArray>',
+ ' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
+ ' </STAT>']),
+ ([
+ dict(
+ tag="wght",
+ name="Weight",
+ ordering=1,
+ values=[]),
+ dict(
+ tag="ABCD",
+ name="ABCDTest",
+ ordering=0,
+ values=[
+ dict(value=100, name="Regular", flags=0x2)])],
+ [dict(location=dict(wght=300, ABCD=100), name='Regular ABCD')], 18, [
+ ' <STAT>',
+ ' <Version value="0x00010002"/>',
+ ' <DesignAxisRecordSize value="8"/>',
+ ' <!-- DesignAxisCount=2 -->',
+ ' <DesignAxisRecord>',
+ ' <Axis index="0">',
+ ' <AxisTag value="wght"/>',
+ ' <AxisNameID value="256"/> <!-- Weight -->',
+ ' <AxisOrdering value="1"/>',
+ ' </Axis>',
+ ' <Axis index="1">',
+ ' <AxisTag value="ABCD"/>',
+ ' <AxisNameID value="257"/> <!-- ABCDTest -->',
+ ' <AxisOrdering value="0"/>',
+ ' </Axis>',
+ ' </DesignAxisRecord>',
+ ' <!-- AxisValueCount=2 -->',
+ ' <AxisValueArray>',
+ ' <AxisValue index="0" Format="4">',
+ ' <!-- AxisCount=2 -->',
+ ' <Flags value="0"/>',
+ ' <ValueNameID value="259"/> <!-- Regular ABCD -->',
+ ' <AxisValueRecord index="0">',
+ ' <AxisIndex value="0"/>',
+ ' <Value value="300.0"/>',
+ ' </AxisValueRecord>',
+ ' <AxisValueRecord index="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Value value="100.0"/>',
+ ' </AxisValueRecord>',
+ ' </AxisValue>',
+ ' <AxisValue index="1" Format="1">',
+ ' <AxisIndex value="1"/>',
+ ' <Flags value="2"/>',
+ ' <ValueNameID value="258"/> <!-- Regular -->',
+ ' <Value value="100.0"/>',
+ ' </AxisValue>',
+ ' </AxisValueArray>',
+ ' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
+ ' </STAT>']),
+]
+
+
+@pytest.mark.parametrize("axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data)
+def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
+ font = ttLib.TTFont()
+ font["name"] = ttLib.newTable("name")
+ font["name"].names = []
+ builder.buildStatTable(font, axes, axisValues, elidedFallbackName)
+ f = io.StringIO()
+ font.saveXML(f, tables=["STAT"])
+ ttx = f.getvalue().splitlines()
+ ttx = ttx[3:-2] # strip XML header and <ttFont> element
+ assert expected_ttx == ttx
+ # Compile and round-trip
+ f = io.BytesIO()
+ font.save(f)
+ font = ttLib.TTFont(f)
+ f = io.StringIO()
+ font.saveXML(f, tables=["STAT"])
+ ttx = f.getvalue().splitlines()
+ ttx = ttx[3:-2] # strip XML header and <ttFont> element
+ assert expected_ttx == ttx
+
+
+def test_stat_infinities():
+ negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
+ assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
+ posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16)
+ assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
+
+
if __name__ == "__main__":
import sys
diff --git a/Tests/subset/data/TestContextSubstFormat3.ttx b/Tests/subset/data/TestContextSubstFormat3.ttx
new file mode 100644
index 00000000..899b037e
--- /dev/null
+++ b/Tests/subset/data/TestContextSubstFormat3.ttx
@@ -0,0 +1,610 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9">
+
+ <GlyphOrder>
+ <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+ <GlyphID id="0" name=".notdef"/>
+ <GlyphID id="1" name="plus"/>
+ <GlyphID id="2" name="glyph00002"/>
+ <GlyphID id="3" name="glyph00003"/>
+ <GlyphID id="4" name="glyph00004"/>
+ <GlyphID id="5" name="glyph00005"/>
+ <GlyphID id="6" name="glyph00006"/>
+ <GlyphID id="7" name="glyph00007"/>
+ </GlyphOrder>
+
+ <head>
+ <!-- Most of this table will be recalculated by the compiler -->
+ <tableVersion value="1.0"/>
+ <fontRevision value="1.0"/>
+ <checkSumAdjustment value="0xa69ed898"/>
+ <magicNumber value="0x5f0f3cf5"/>
+ <flags value="00000000 00001111"/>
+ <unitsPerEm value="1000"/>
+ <created value="Mon Nov 21 06:10:39 2016"/>
+ <modified value="Fri Apr 24 05:31:23 2020"/>
+ <xMin value="-1000"/>
+ <yMin value="-509"/>
+ <xMax value="1135"/>
+ <yMax value="1194"/>
+ <macStyle value="00000000 00000000"/>
+ <lowestRecPPEM value="8"/>
+ <fontDirectionHint value="0"/>
+ <indexToLocFormat value="0"/>
+ <glyphDataFormat value="0"/>
+ </head>
+
+ <hhea>
+ <tableVersion value="0x00010000"/>
+ <ascent value="977"/>
+ <descent value="-205"/>
+ <lineGap value="67"/>
+ <advanceWidthMax value="1000"/>
+ <minLeftSideBearing value="-1000"/>
+ <minRightSideBearing value="-1000"/>
+ <xMaxExtent value="1135"/>
+ <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="8"/>
+ <maxPoints value="240"/>
+ <maxContours value="41"/>
+ <maxCompositePoints value="163"/>
+ <maxCompositeContours value="12"/>
+ <maxZones value="1"/>
+ <maxTwilightPoints value="0"/>
+ <maxStorage value="0"/>
+ <maxFunctionDefs value="0"/>
+ <maxInstructionDefs value="0"/>
+ <maxStackElements value="0"/>
+ <maxSizeOfInstructions value="0"/>
+ <maxComponentElements value="4"/>
+ <maxComponentDepth value="3"/>
+ </maxp>
+
+ <OS_2>
+ <!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
+ will be recalculated by the compiler -->
+ <version value="4"/>
+ <xAvgCharWidth value="500"/>
+ <usWeightClass value="500"/>
+ <usWidthClass value="5"/>
+ <fsType value="00000000 00000000"/>
+ <ySubscriptXSize value="665"/>
+ <ySubscriptYSize value="716"/>
+ <ySubscriptXOffset value="0"/>
+ <ySubscriptYOffset value="143"/>
+ <ySuperscriptXSize value="0"/>
+ <ySuperscriptYSize value="0"/>
+ <ySuperscriptXOffset value="0"/>
+ <ySuperscriptYOffset value="0"/>
+ <yStrikeoutSize value="51"/>
+ <yStrikeoutPosition value="265"/>
+ <sFamilyClass value="2057"/>
+ <panose>
+ <bFamilyType value="2"/>
+ <bSerifStyle value="0"/>
+ <bWeight value="6"/>
+ <bProportion value="9"/>
+ <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="BE5N"/>
+ <fsSelection value="00000000 11000000"/>
+ <usFirstCharIndex value="43"/>
+ <usLastCharIndex value="43"/>
+ <sTypoAscender value="977"/>
+ <sTypoDescender value="-272"/>
+ <sTypoLineGap value="0"/>
+ <usWinAscent value="977"/>
+ <usWinDescent value="272"/>
+ <ulCodePageRange1 value="00100000 00000000 00000001 00011111"/>
+ <ulCodePageRange2 value="11000100 00000000 00000000 00000000"/>
+ <sxHeight value="530"/>
+ <sCapHeight value="735"/>
+ <usDefaultChar value="0"/>
+ <usBreakChar value="32"/>
+ <usMaxContext value="8"/>
+ </OS_2>
+
+ <hmtx>
+ <mtx name=".notdef" width="500" lsb="57"/>
+ <mtx name="glyph00002" width="500" lsb="57"/>
+ <mtx name="glyph00003" width="500" lsb="57"/>
+ <mtx name="glyph00004" width="500" lsb="-8"/>
+ <mtx name="glyph00005" width="500" lsb="-8"/>
+ <mtx name="glyph00006" width="500" lsb="-8"/>
+ <mtx name="glyph00007" width="500" lsb="-65"/>
+ <mtx name="plus" width="500" lsb="57"/>
+ </hmtx>
+
+ <cmap>
+ <tableVersion version="0"/>
+ <cmap_format_4 platformID="0" platEncID="3" language="0">
+ <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
+ </cmap_format_4>
+ <cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="28" language="0" nGroups="1">
+ <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
+ </cmap_format_12>
+ <cmap_format_4 platformID="3" platEncID="1" language="0">
+ <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
+ </cmap_format_4>
+ <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="28" language="0" nGroups="1">
+ <map code="0x2b" name="plus"/><!-- PLUS SIGN -->
+ </cmap_format_12>
+ </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"/><!-- contains no outline data -->
+
+ <TTGlyph name="glyph00002" xMin="57" yMin="139" xMax="508" yMax="541">
+ <contour>
+ <pt x="203" y="139" on="1"/>
+ <pt x="203" y="298" on="1"/>
+ <pt x="57" y="298" on="1"/>
+ <pt x="57" y="382" on="1"/>
+ <pt x="203" y="382" on="1"/>
+ <pt x="203" y="541" on="1"/>
+ <pt x="297" y="541" on="1"/>
+ <pt x="297" y="382" on="1"/>
+ <pt x="508" y="382" on="1"/>
+ <pt x="508" y="298" on="1"/>
+ <pt x="297" y="298" on="1"/>
+ <pt x="297" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="glyph00003" xMin="57" yMin="139" xMax="508" yMax="541">
+ <contour>
+ <pt x="260" y="139" on="1"/>
+ <pt x="260" y="298" on="1"/>
+ <pt x="57" y="298" on="1"/>
+ <pt x="57" y="382" on="1"/>
+ <pt x="260" y="382" on="1"/>
+ <pt x="260" y="541" on="1"/>
+ <pt x="354" y="541" on="1"/>
+ <pt x="354" y="382" on="1"/>
+ <pt x="508" y="382" on="1"/>
+ <pt x="508" y="298" on="1"/>
+ <pt x="354" y="298" on="1"/>
+ <pt x="354" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="glyph00004" xMin="-8" yMin="139" xMax="508" yMax="541">
+ <contour>
+ <pt x="203" y="139" on="1"/>
+ <pt x="203" y="298" on="1"/>
+ <pt x="-8" y="298" on="1"/>
+ <pt x="-8" y="382" on="1"/>
+ <pt x="203" y="382" on="1"/>
+ <pt x="203" y="541" on="1"/>
+ <pt x="297" y="541" on="1"/>
+ <pt x="297" y="382" on="1"/>
+ <pt x="508" y="382" on="1"/>
+ <pt x="508" y="298" on="1"/>
+ <pt x="297" y="298" on="1"/>
+ <pt x="297" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="glyph00005" xMin="-8" yMin="139" xMax="443" yMax="541">
+ <contour>
+ <pt x="203" y="139" on="1"/>
+ <pt x="203" y="298" on="1"/>
+ <pt x="-8" y="298" on="1"/>
+ <pt x="-8" y="382" on="1"/>
+ <pt x="203" y="382" on="1"/>
+ <pt x="203" y="541" on="1"/>
+ <pt x="297" y="541" on="1"/>
+ <pt x="297" y="382" on="1"/>
+ <pt x="443" y="382" on="1"/>
+ <pt x="443" y="298" on="1"/>
+ <pt x="297" y="298" on="1"/>
+ <pt x="297" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="glyph00006" xMin="-8" yMin="139" xMax="443" yMax="541">
+ <contour>
+ <pt x="146" y="139" on="1"/>
+ <pt x="146" y="298" on="1"/>
+ <pt x="-8" y="298" on="1"/>
+ <pt x="-8" y="382" on="1"/>
+ <pt x="146" y="382" on="1"/>
+ <pt x="146" y="541" on="1"/>
+ <pt x="240" y="541" on="1"/>
+ <pt x="240" y="382" on="1"/>
+ <pt x="443" y="382" on="1"/>
+ <pt x="443" y="298" on="1"/>
+ <pt x="240" y="298" on="1"/>
+ <pt x="240" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="glyph00007" xMin="-65" yMin="139" xMax="443" yMax="541">
+ <contour>
+ <pt x="203" y="139" on="1"/>
+ <pt x="203" y="298" on="1"/>
+ <pt x="-65" y="298" on="1"/>
+ <pt x="-65" y="382" on="1"/>
+ <pt x="203" y="382" on="1"/>
+ <pt x="203" y="541" on="1"/>
+ <pt x="297" y="541" on="1"/>
+ <pt x="297" y="382" on="1"/>
+ <pt x="443" y="382" on="1"/>
+ <pt x="443" y="298" on="1"/>
+ <pt x="297" y="298" on="1"/>
+ <pt x="297" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ <TTGlyph name="plus" xMin="57" yMin="139" xMax="443" yMax="541">
+ <contour>
+ <pt x="203" y="139" on="1"/>
+ <pt x="203" y="298" on="1"/>
+ <pt x="57" y="298" on="1"/>
+ <pt x="57" y="382" on="1"/>
+ <pt x="203" y="382" on="1"/>
+ <pt x="203" y="541" on="1"/>
+ <pt x="297" y="541" on="1"/>
+ <pt x="297" y="382" on="1"/>
+ <pt x="443" y="382" on="1"/>
+ <pt x="443" y="298" on="1"/>
+ <pt x="297" y="298" on="1"/>
+ <pt x="297" y="139" on="1"/>
+ </contour>
+ <instructions/>
+ </TTGlyph>
+
+ </glyf>
+
+ <name>
+ <namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
+ Copyright (c) 2015-2019 Belleve Invis.
+ </namerecord>
+ <namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
+ Iosevka Medium
+ </namerecord>
+ <namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
+ Regular
+ </namerecord>
+ <namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
+ Iosevka Medium Version 3.0.0-rc.8
+ </namerecord>
+ <namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
+ Iosevka Medium
+ </namerecord>
+ <namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
+ Version 3.0.0-rc.8; ttfautohint (v1.8.3)
+ </namerecord>
+ <namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
+ Iosevka-Medium
+ </namerecord>
+ </name>
+
+ <post>
+ <formatType value="3.0"/>
+ <italicAngle value="0.0"/>
+ <underlinePosition value="-50"/>
+ <underlineThickness value="50"/>
+ <isFixedPitch value="1"/>
+ <minMemType42 value="0"/>
+ <maxMemType42 value="6380"/>
+ <minMemType1 value="0"/>
+ <maxMemType1 value="1"/>
+ </post>
+
+ <gasp>
+ <gaspRange rangeMaxPPEM="65535" rangeGaspBehavior="15"/>
+ </gasp>
+
+ <GDEF>
+ <Version value="0x00010000"/>
+ <GlyphClassDef Format="2">
+ <ClassDef glyph=".notdef" class="1"/>
+ <ClassDef glyph="glyph00002" class="1"/>
+ <ClassDef glyph="glyph00003" class="1"/>
+ <ClassDef glyph="glyph00004" class="1"/>
+ <ClassDef glyph="glyph00005" class="1"/>
+ <ClassDef glyph="glyph00006" class="1"/>
+ <ClassDef glyph="glyph00007" class="1"/>
+ <ClassDef glyph="plus" class="1"/>
+ </GlyphClassDef>
+ </GDEF>
+
+ <GPOS>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=0 -->
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ </ScriptList>
+ <FeatureList>
+ <!-- FeatureCount=0 -->
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=0 -->
+ </LookupList>
+ </GPOS>
+
+ <GSUB>
+ <Version value="0x00010000"/>
+ <ScriptList>
+ <!-- ScriptCount=4 -->
+ <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="cyrl"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="2">
+ <ScriptTag value="grek"/>
+ <Script>
+ <DefaultLangSys>
+ <ReqFeatureIndex value="65535"/>
+ <!-- FeatureCount=1 -->
+ <FeatureIndex index="0" value="0"/>
+ </DefaultLangSys>
+ <!-- LangSysCount=0 -->
+ </Script>
+ </ScriptRecord>
+ <ScriptRecord index="3">
+ <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="calt"/>
+ <Feature>
+ <!-- LookupCount=2 -->
+ <LookupListIndex index="0" value="0"/>
+ <LookupListIndex index="1" value="1"/>
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=6 -->
+ <Lookup index="0">
+ <LookupType value="6"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <ChainContextSubst index="0" Format="2">
+ <Coverage Format="1">
+ <Glyph value="plus"/>
+ </Coverage>
+ <BacktrackClassDef Format="1">
+ <ClassDef glyph="glyph00005" class="1"/>
+ <ClassDef glyph="glyph00007" class="1"/>
+ </BacktrackClassDef>
+ <InputClassDef Format="1">
+ <ClassDef glyph="plus" class="1"/>
+ </InputClassDef>
+ <LookAheadClassDef Format="2">
+ </LookAheadClassDef>
+ <!-- ChainSubClassSetCount=2 -->
+ <ChainSubClassSet index="0" empty="1"/>
+ <ChainSubClassSet index="1">
+ <!-- ChainSubClassRuleCount=4 -->
+ <ChainSubClassRule index="0">
+ <!-- BacktrackGlyphCount=1 -->
+ <Backtrack index="0" value="1"/>
+ <!-- InputGlyphCount=1 -->
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- SubstCount=1 -->
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="5"/>
+ </SubstLookupRecord>
+ </ChainSubClassRule>
+ <ChainSubClassRule index="1">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=4 -->
+ <Input index="0" value="1"/>
+ <Input index="1" value="1"/>
+ <Input index="2" value="1"/>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- SubstCount=4 -->
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="4"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="1">
+ <SequenceIndex value="1"/>
+ <LookupListIndex value="3"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="2">
+ <SequenceIndex value="2"/>
+ <LookupListIndex value="3"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="3">
+ <SequenceIndex value="3"/>
+ <LookupListIndex value="2"/>
+ </SubstLookupRecord>
+ </ChainSubClassRule>
+ <ChainSubClassRule index="2">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=3 -->
+ <Input index="0" value="1"/>
+ <Input index="1" value="1"/>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- SubstCount=3 -->
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="4"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="1">
+ <SequenceIndex value="1"/>
+ <LookupListIndex value="3"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="2">
+ <SequenceIndex value="2"/>
+ <LookupListIndex value="2"/>
+ </SubstLookupRecord>
+ </ChainSubClassRule>
+ <ChainSubClassRule index="3">
+ <!-- BacktrackGlyphCount=0 -->
+ <!-- InputGlyphCount=2 -->
+ <Input index="0" value="1"/>
+ <!-- LookAheadGlyphCount=0 -->
+ <!-- SubstCount=2 -->
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="4"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="1">
+ <SequenceIndex value="1"/>
+ <LookupListIndex value="2"/>
+ </SubstLookupRecord>
+ </ChainSubClassRule>
+ </ChainSubClassSet>
+ </ChainContextSubst>
+ </Lookup>
+ <Lookup index="1">
+ <LookupType value="5"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=2 -->
+ <ContextSubst index="0" Format="3">
+ <!-- GlyphCount=3 -->
+ <!-- SubstCount=2 -->
+ <Coverage index="0" Format="1">
+ <Glyph value="glyph00002"/>
+ </Coverage>
+ <Coverage index="1" Format="1">
+ <Glyph value="glyph00004"/>
+ </Coverage>
+ <Coverage index="2" Format="1">
+ <Glyph value="glyph00005"/>
+ </Coverage>
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="5"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="1">
+ <SequenceIndex value="2"/>
+ <LookupListIndex value="5"/>
+ </SubstLookupRecord>
+ </ContextSubst>
+ <ContextSubst index="1" Format="3">
+ <!-- GlyphCount=2 -->
+ <!-- SubstCount=2 -->
+ <Coverage index="0" Format="1">
+ <Glyph value="glyph00002"/>
+ </Coverage>
+ <Coverage index="1" Format="1">
+ <Glyph value="glyph00005"/>
+ </Coverage>
+ <SubstLookupRecord index="0">
+ <SequenceIndex value="0"/>
+ <LookupListIndex value="5"/>
+ </SubstLookupRecord>
+ <SubstLookupRecord index="1">
+ <SequenceIndex value="1"/>
+ <LookupListIndex value="5"/>
+ </SubstLookupRecord>
+ </ContextSubst>
+ </Lookup>
+ <Lookup index="2">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="plus" out="glyph00005"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="3">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="plus" out="glyph00004"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="4">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="plus" out="glyph00002"/>
+ </SingleSubst>
+ </Lookup>
+ <Lookup index="5">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="2">
+ <Substitution in="glyph00002" out="glyph00003"/>
+ <Substitution in="glyph00005" out="glyph00006"/>
+ <Substitution in="plus" out="glyph00007"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py
index 2493daff..2cce9baf 100644
--- a/Tests/subset/subset_test.py
+++ b/Tests/subset/subset_test.py
@@ -56,7 +56,7 @@ class SubsetTest(unittest.TestCase):
lines.append(line.rstrip() + os.linesep)
return lines
- def expect_ttx(self, font, expected_ttx, tables):
+ def expect_ttx(self, font, expected_ttx, tables=None):
path = self.temp_path(suffix=".ttx")
font.saveXML(path, tables=tables)
actual = self.read_ttx(path)
@@ -732,6 +732,17 @@ class SubsetTest(unittest.TestCase):
self.assertEqual(ttf.flavor, 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")
+ subsetpath = self.temp_path(".ttf")
+ subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath])
+ subsetfont = TTFont(subsetpath)
+ # check all glyphs are kept via GSUB closure, no changes expected
+ self.expect_ttx(subsetfont, ttx)
+
@pytest.fixture
def featureVarsTestFont():
diff --git a/Tests/ttLib/tables/_g_v_a_r_test.py b/Tests/ttLib/tables/_g_v_a_r_test.py
index 6de6e247..9a00fded 100644
--- a/Tests/ttLib/tables/_g_v_a_r_test.py
+++ b/Tests/ttLib/tables/_g_v_a_r_test.py
@@ -81,15 +81,6 @@ GVAR_VARIATIONS = {
GVAR_XML = [
'<version value="1"/>',
'<reserved value="0"/>',
- '<glyphVariations glyph="space">',
- ' <tuple>',
- ' <coord axis="wdth" value="0.7"/>',
- ' <delta pt="0" x="1" y="11"/>',
- ' <delta pt="1" x="2" y="22"/>',
- ' <delta pt="2" x="3" y="33"/>',
- ' <delta pt="3" x="4" y="44"/>',
- ' </tuple>',
- '</glyphVariations>',
'<glyphVariations glyph="I">',
' <tuple>',
' <coord axis="wght" min="0.0" value="0.5" max="1.0"/>',
@@ -113,6 +104,15 @@ GVAR_XML = [
' <delta pt="7" x="1" y="11"/>',
' </tuple>',
'</glyphVariations>',
+ '<glyphVariations glyph="space">',
+ ' <tuple>',
+ ' <coord axis="wdth" value="0.7"/>',
+ ' <delta pt="0" x="1" y="11"/>',
+ ' <delta pt="1" x="2" y="22"/>',
+ ' <delta pt="2" x="3" y="33"/>',
+ ' <delta pt="3" x="4" y="44"/>',
+ ' </tuple>',
+ '</glyphVariations>',
]
diff --git a/Tests/ttLib/tables/_m_e_t_a_test.py b/Tests/ttLib/tables/_m_e_t_a_test.py
index 4f0a4158..3a4f2f5b 100644
--- a/Tests/ttLib/tables/_m_e_t_a_test.py
+++ b/Tests/ttLib/tables/_m_e_t_a_test.py
@@ -58,6 +58,19 @@ class MetaTableTest(unittest.TestCase):
'</hexdata>'
], [line.strip() for line in xml.splitlines()][1:])
+ def test_toXML_ascii_data(self):
+ table = table__m_e_t_a()
+ table.data["TEST"] = b"Hello!"
+ writer = XMLWriter(BytesIO())
+ table.toXML(writer, {"meta": table})
+ xml = writer.file.getvalue().decode("utf-8")
+ self.assertEqual([
+ '<hexdata tag="TEST">',
+ '<!-- ascii: Hello! -->',
+ '48656c6c 6f21',
+ '</hexdata>'
+ ], [line.strip() for line in xml.splitlines()][1:])
+
def test_fromXML(self):
table = table__m_e_t_a()
for name, attrs, content in parseXML(
diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py
index d770a523..5f5c965c 100644
--- a/Tests/ttLib/tables/_n_a_m_e_test.py
+++ b/Tests/ttLib/tables/_n_a_m_e_test.py
@@ -144,6 +144,48 @@ class NameTableTest(unittest.TestCase):
rec2 = table.getName(2, 1, 0, 0)
self.assertEqual(str(rec2), "Regular")
+ @staticmethod
+ def _get_test_names():
+ names = {
+ "en": "Width",
+ "de-CH": "Breite",
+ "gsw-LI": "Bräiti",
+ }
+ namesSubSet = names.copy()
+ del namesSubSet["gsw-LI"]
+ namesSuperSet = names.copy()
+ namesSuperSet["nl"] = "Breedte"
+ return names, namesSubSet, namesSuperSet
+
+ def test_findMultilingualName(self):
+ table = table__n_a_m_e()
+ names, namesSubSet, namesSuperSet = self._get_test_names()
+ nameID = table.addMultilingualName(names)
+ assert nameID is not None
+ self.assertEqual(nameID, table.findMultilingualName(names))
+ self.assertEqual(nameID, table.findMultilingualName(namesSubSet))
+ self.assertEqual(None, table.findMultilingualName(namesSuperSet))
+
+ def test_addMultilingualNameReuse(self):
+ table = table__n_a_m_e()
+ names, namesSubSet, namesSuperSet = self._get_test_names()
+ nameID = table.addMultilingualName(names)
+ assert nameID is not None
+ self.assertEqual(nameID, table.addMultilingualName(names))
+ self.assertEqual(nameID, table.addMultilingualName(namesSubSet))
+ self.assertNotEqual(None, table.addMultilingualName(namesSuperSet))
+
+ def test_findMultilingualNameNoMac(self):
+ table = table__n_a_m_e()
+ names, namesSubSet, namesSuperSet = self._get_test_names()
+ nameID = table.addMultilingualName(names, mac=False)
+ assert nameID is not None
+ self.assertEqual(nameID, table.findMultilingualName(names, mac=False))
+ self.assertEqual(None, table.findMultilingualName(names))
+ self.assertEqual(nameID, table.findMultilingualName(namesSubSet, mac=False))
+ self.assertEqual(None, table.findMultilingualName(namesSubSet))
+ self.assertEqual(None, table.findMultilingualName(namesSuperSet))
+
def test_addMultilingualName(self):
# Microsoft Windows has language codes for “English” (en)
# and for “Standard German as used in Switzerland” (de-CH).
diff --git a/Tests/ufoLib/glifLib_test.py b/Tests/ufoLib/glifLib_test.py
index a29c76bf..0b12a064 100644
--- a/Tests/ufoLib/glifLib_test.py
+++ b/Tests/ufoLib/glifLib_test.py
@@ -1,3 +1,4 @@
+import logging
import os
import tempfile
import shutil
@@ -7,7 +8,9 @@ from .testSupport import getDemoFontGlyphSetPath
from fontTools.ufoLib.glifLib import (
GlyphSet, glyphNameToFileName, readGlyphFromString, writeGlyphToString,
)
+from fontTools.ufoLib.errors import GlifLibError, UnsupportedGLIFFormat, UnsupportedUFOFormat
from fontTools.misc.etree import XML_DECLARATION
+import pytest
GLYPHSETDIR = getDemoFontGlyphSetPath()
@@ -142,9 +145,9 @@ class _Glyph:
pass
-class ReadWriteFuncTest(unittest.TestCase):
+class ReadWriteFuncTest:
- def testRoundTrip(self):
+ def test_roundtrip(self):
glyph = _Glyph()
glyph.name = "a"
glyph.unicodes = [0x0061]
@@ -153,29 +156,105 @@ class ReadWriteFuncTest(unittest.TestCase):
glyph2 = _Glyph()
readGlyphFromString(s1, glyph2)
- self.assertEqual(glyph.__dict__, glyph2.__dict__)
+ assert glyph.__dict__ == glyph2.__dict__
s2 = writeGlyphToString(glyph2.name, glyph2)
- self.assertEqual(s1, s2)
+ assert s1 == s2
- def testXmlDeclaration(self):
+ def test_xml_declaration(self):
s = writeGlyphToString("a", _Glyph())
- self.assertTrue(s.startswith(XML_DECLARATION % "UTF-8"))
+ assert s.startswith(XML_DECLARATION % "UTF-8")
+
+ def test_parse_xml_remove_comments(self):
+ s = b"""<?xml version='1.0' encoding='UTF-8'?>
+ <!-- a comment -->
+ <glyph name="A" format="2">
+ <advance width="1290"/>
+ <unicode hex="0041"/>
+ <!-- another comment -->
+ </glyph>
+ """
+
+ g = _Glyph()
+ readGlyphFromString(s, g)
+
+ assert g.name == "A"
+ assert g.width == 1290
+ assert g.unicodes == [0x0041]
+
+ def test_read_unsupported_format_version(self, caplog):
+ s = """<?xml version='1.0' encoding='utf-8'?>
+ <glyph name="A" format="0" formatMinor="0">
+ <advance width="500"/>
+ <unicode hex="0041"/>
+ </glyph>
+ """
+
+ with pytest.raises(UnsupportedGLIFFormat):
+ readGlyphFromString(s, _Glyph()) # validate=True by default
+
+ with pytest.raises(UnsupportedGLIFFormat):
+ readGlyphFromString(s, _Glyph(), validate=True)
+
+ caplog.clear()
+ with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib.glifLib"):
+ readGlyphFromString(s, _Glyph(), validate=False)
+
+ assert len(caplog.records) == 1
+ assert "Unsupported GLIF format" in caplog.text
+ assert "Assuming the latest supported version" in caplog.text
+
+ def test_read_allow_format_versions(self):
+ s = """<?xml version='1.0' encoding='utf-8'?>
+ <glyph name="A" format="2">
+ <advance width="500"/>
+ <unicode hex="0041"/>
+ </glyph>
+ """
+
+ # these two calls are are equivalent
+ readGlyphFromString(s, _Glyph(), formatVersions=[1, 2])
+ readGlyphFromString(s, _Glyph(), formatVersions=[(1, 0), (2, 0)])
+
+ # if at least one supported formatVersion, unsupported ones are ignored
+ readGlyphFromString(s, _Glyph(), formatVersions=[(2, 0), (123, 456)])
+
+ with pytest.raises(
+ ValueError,
+ match="None of the requested GLIF formatVersions are supported"
+ ):
+ readGlyphFromString(s, _Glyph(), formatVersions=[0, 2001])
+
+ with pytest.raises(GlifLibError, match="Forbidden GLIF format version"):
+ readGlyphFromString(s, _Glyph(), formatVersions=[1])
+
+
+def test_GlyphSet_unsupported_ufoFormatVersion(tmp_path, caplog):
+ with pytest.raises(UnsupportedUFOFormat):
+ GlyphSet(tmp_path, ufoFormatVersion=0)
+ with pytest.raises(UnsupportedUFOFormat):
+ GlyphSet(tmp_path, ufoFormatVersion=(0, 1))
+
+def test_GlyphSet_writeGlyph_formatVersion(tmp_path):
+ src = GlyphSet(GLYPHSETDIR)
+ dst = GlyphSet(tmp_path, ufoFormatVersion=(2, 0))
+ glyph = src["A"]
-def test_parse_xml_remove_comments():
- s = b"""<?xml version='1.0' encoding='UTF-8'?>
- <!-- a comment -->
- <glyph name="A" format="2">
- <advance width="1290"/>
- <unicode hex="0041"/>
- <!-- another comment -->
- </glyph>
- """
+ # no explicit formatVersion passed: use the more recent GLIF formatVersion
+ # that is supported by given ufoFormatVersion (GLIF 1 for UFO 2)
+ dst.writeGlyph("A", glyph)
+ glif = dst.getGLIF("A")
+ assert b'format="1"' in glif
+ assert b'formatMinor' not in glif # omitted when 0
- g = _Glyph()
- readGlyphFromString(s, g)
+ # explicit, unknown formatVersion
+ with pytest.raises(UnsupportedGLIFFormat):
+ dst.writeGlyph("A", glyph, formatVersion=(0, 0))
- assert g.name == "A"
- assert g.width == 1290
- assert g.unicodes == [0x0041]
+ # explicit, known formatVersion but unsupported by given ufoFormatVersion
+ with pytest.raises(
+ UnsupportedGLIFFormat,
+ match="Unsupported GLIF format version .*for UFO format version",
+ ):
+ dst.writeGlyph("A", glyph, formatVersion=(2, 0))
diff --git a/Tests/ufoLib/ufoLib_test.py b/Tests/ufoLib/ufoLib_test.py
new file mode 100644
index 00000000..430e7a7d
--- /dev/null
+++ b/Tests/ufoLib/ufoLib_test.py
@@ -0,0 +1,98 @@
+import logging
+import shutil
+
+from fontTools.misc import plistlib
+from fontTools.ufoLib import UFOReader, UFOWriter, UFOFormatVersion
+from fontTools.ufoLib.errors import UFOLibError, UnsupportedUFOFormat
+import pytest
+
+
+@pytest.fixture
+def ufo_path(tmp_path):
+ ufodir = tmp_path / "TestFont.ufo"
+ ufodir.mkdir()
+ with (ufodir / "metainfo.plist").open("wb") as f:
+ plistlib.dump({"creator": "pytest", "formatVersion": 3}, f)
+ (ufodir / "glyphs").mkdir()
+ with (ufodir / "layercontents.plist").open("wb") as f:
+ plistlib.dump([("public.default", "glyphs")], f)
+ return ufodir
+
+
+def test_formatVersion_deprecated(ufo_path):
+ reader = UFOReader(ufo_path)
+
+ with pytest.warns(DeprecationWarning) as warnings:
+ assert reader.formatVersion == 3
+
+ assert len(warnings) == 1
+ assert "is deprecated; use the 'formatVersionTuple'" in warnings[0].message.args[0]
+
+
+def test_formatVersionTuple(ufo_path):
+ reader = UFOReader(ufo_path)
+
+ assert reader.formatVersionTuple == (3, 0)
+ assert reader.formatVersionTuple.major == 3
+ assert reader.formatVersionTuple.minor == 0
+ assert str(reader.formatVersionTuple) == "3.0"
+
+
+def test_readMetaInfo_errors(ufo_path):
+ (ufo_path / "metainfo.plist").unlink()
+ with pytest.raises(UFOLibError, match="'metainfo.plist' is missing"):
+ UFOReader(ufo_path)
+
+ (ufo_path / "metainfo.plist").write_bytes(plistlib.dumps({}))
+ with pytest.raises(UFOLibError, match="Missing required formatVersion"):
+ UFOReader(ufo_path)
+
+ (ufo_path / "metainfo.plist").write_bytes(plistlib.dumps([]))
+ with pytest.raises(UFOLibError, match="metainfo.plist is not properly formatted"):
+ UFOReader(ufo_path)
+
+
+def test_readMetaInfo_unsupported_format_version(ufo_path, caplog):
+ metainfo = {"formatVersion": 10, "formatVersionMinor": 15}
+ (ufo_path / "metainfo.plist").write_bytes(plistlib.dumps(metainfo))
+
+ with pytest.raises(UnsupportedUFOFormat):
+ UFOReader(ufo_path) # validate=True by default
+
+ with pytest.raises(UnsupportedUFOFormat):
+ UFOReader(ufo_path, validate=True)
+
+ caplog.clear()
+ with caplog.at_level(logging.WARNING, logger="fontTools.ufoLib"):
+ UFOReader(ufo_path, validate=False)
+
+ assert len(caplog.records) == 1
+ assert "Unsupported UFO format" in caplog.text
+ assert "Assuming the latest supported version" in caplog.text
+
+
+def test_UFOWriter_formatVersion(tmp_path):
+ ufo_path = tmp_path / "TestFont.ufo"
+ with UFOWriter(ufo_path, formatVersion=3) as writer:
+ assert writer.formatVersionTuple == (3, 0)
+
+ shutil.rmtree(str(ufo_path))
+ with UFOWriter(ufo_path, formatVersion=(2, 0)) as writer:
+ assert writer.formatVersionTuple == (2, 0)
+
+
+def test_UFOWriter_formatVersion_default_latest(tmp_path):
+ writer = UFOWriter(tmp_path / "TestFont.ufo")
+ assert writer.formatVersionTuple == UFOFormatVersion.default()
+
+
+def test_UFOWriter_unsupported_format_version(tmp_path):
+ with pytest.raises(UnsupportedUFOFormat):
+ UFOWriter(tmp_path, formatVersion=(123, 456))
+
+
+def test_UFOWriter_previous_higher_format_version(ufo_path):
+ with pytest.raises(
+ UnsupportedUFOFormat, match="UFO located at this path is a higher version"
+ ):
+ UFOWriter(ufo_path, formatVersion=(2, 0))
diff --git a/Tests/varLib/data/FeatureVarsWholeRange.designspace b/Tests/varLib/data/FeatureVarsWholeRange.designspace
new file mode 100644
index 00000000..2d8802cf
--- /dev/null
+++ b/Tests/varLib/data/FeatureVarsWholeRange.designspace
@@ -0,0 +1,34 @@
+<?xml version='1.0' encoding='utf-8'?>
+<designspace format="3">
+ <axes>
+ <axis default="368.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght" />
+ </axes>
+ <rules processing="last">
+ <rule name="always">
+ <conditionset>
+ <condition name="weight" minimum="0" maximum="1000" />
+ </conditionset>
+ <sub name="uni0024" with="uni0024.nostroke" />
+ </rule>
+ </rules>
+ <sources>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master0.ufo" name="master_0" stylename="Master0">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master1.ufo" name="master_1" stylename="Master1">
+ <lib copy="1" />
+ <groups copy="1" />
+ <info copy="1" />
+ <location>
+ <dimension name="weight" xvalue="368" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master3.ufo" name="master_3" stylename="Master3">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/FeatureVarsWholeRangeEmpty.designspace b/Tests/varLib/data/FeatureVarsWholeRangeEmpty.designspace
new file mode 100644
index 00000000..a692daa6
--- /dev/null
+++ b/Tests/varLib/data/FeatureVarsWholeRangeEmpty.designspace
@@ -0,0 +1,33 @@
+<?xml version='1.0' encoding='utf-8'?>
+<designspace format="3">
+ <axes>
+ <axis default="368.0" maximum="1000.0" minimum="0.0" name="weight" tag="wght" />
+ </axes>
+ <rules processing="last">
+ <rule name="always">
+ <conditionset>
+ </conditionset>
+ <sub name="uni0024" with="uni0024.nostroke" />
+ </rule>
+ </rules>
+ <sources>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master0.ufo" name="master_0" stylename="Master0">
+ <location>
+ <dimension name="weight" xvalue="0" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master1.ufo" name="master_1" stylename="Master1">
+ <lib copy="1" />
+ <groups copy="1" />
+ <info copy="1" />
+ <location>
+ <dimension name="weight" xvalue="368" />
+ </location>
+ </source>
+ <source familyname="Test Family" filename="master_ufo/TestFamily-Master3.ufo" name="master_3" stylename="Master3">
+ <location>
+ <dimension name="weight" xvalue="1000" />
+ </location>
+ </source>
+ </sources>
+</designspace>
diff --git a/Tests/varLib/data/test_results/Build.ttx b/Tests/varLib/data/test_results/Build.ttx
index 6e9c6e37..5a406c84 100644
--- a/Tests/varLib/data/test_results/Build.ttx
+++ b/Tests/varLib/data/test_results/Build.ttx
@@ -405,509 +405,6 @@
<delta pt="3" x="0" y="0"/>
</tuple>
</glyphVariations>
- <glyphVariations glyph="uni0041">
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <delta pt="0" x="7" y="0"/>
- <delta pt="1" x="7" y="-20"/>
- <delta pt="2" x="-6" y="-29"/>
- <delta pt="3" x="-12" y="-29"/>
- <delta pt="4" x="-25" y="-20"/>
- <delta pt="5" x="-25" y="0"/>
- <delta pt="6" x="14" y="0"/>
- <delta pt="7" x="4" y="9"/>
- <delta pt="8" x="-36" y="9"/>
- <delta pt="9" x="-37" y="0"/>
- <delta pt="10" x="24" y="0"/>
- <delta pt="11" x="9" y="58"/>
- <delta pt="12" x="3" y="68"/>
- <delta pt="13" x="-4" y="0"/>
- <delta pt="14" x="3" y="28"/>
- <delta pt="15" x="-4" y="2"/>
- <delta pt="16" x="4" y="2"/>
- <delta pt="17" x="-4" y="28"/>
- <delta pt="18" x="20" y="0"/>
- <delta pt="19" x="20" y="-20"/>
- <delta pt="20" x="14" y="-29"/>
- <delta pt="21" x="8" y="-29"/>
- <delta pt="22" x="-2" y="-20"/>
- <delta pt="23" x="-2" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="-10" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="5" y="0"/>
- <delta pt="1" x="5" y="19"/>
- <delta pt="2" x="9" y="19"/>
- <delta pt="3" x="6" y="19"/>
- <delta pt="4" x="-15" y="19"/>
- <delta pt="5" x="-15" y="0"/>
- <delta pt="6" x="-6" y="0"/>
- <delta pt="7" x="-14" y="-23"/>
- <delta pt="8" x="46" y="-23"/>
- <delta pt="9" x="39" y="0"/>
- <delta pt="10" x="-69" y="0"/>
- <delta pt="11" x="-27" y="-86"/>
- <delta pt="12" x="-7" y="-16"/>
- <delta pt="13" x="11" y="0"/>
- <delta pt="14" x="-2" y="-39"/>
- <delta pt="15" x="-1" y="-22"/>
- <delta pt="16" x="-1" y="-22"/>
- <delta pt="17" x="8" y="-39"/>
- <delta pt="18" x="-41" y="0"/>
- <delta pt="19" x="-41" y="16"/>
- <delta pt="20" x="-59" y="16"/>
- <delta pt="21" x="6" y="16"/>
- <delta pt="22" x="12" y="16"/>
- <delta pt="23" x="12" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="17" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="2" y="0"/>
- <delta pt="1" x="2" y="-9"/>
- <delta pt="2" x="-4" y="-9"/>
- <delta pt="3" x="-4" y="-9"/>
- <delta pt="4" x="-2" y="-9"/>
- <delta pt="5" x="-2" y="0"/>
- <delta pt="6" x="2" y="0"/>
- <delta pt="7" x="-4" y="0"/>
- <delta pt="8" x="-4" y="0"/>
- <delta pt="9" x="-4" y="0"/>
- <delta pt="10" x="-4" y="0"/>
- <delta pt="11" x="-6" y="8"/>
- <delta pt="12" x="-10" y="0"/>
- <delta pt="13" x="-2" y="0"/>
- <delta pt="14" x="0" y="5"/>
- <delta pt="15" x="-3" y="-5"/>
- <delta pt="16" x="5" y="-5"/>
- <delta pt="17" x="-1" y="5"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="0" y="-8"/>
- <delta pt="20" x="4" y="-8"/>
- <delta pt="21" x="0" y="-8"/>
- <delta pt="22" x="0" y="-8"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="-2" y="0"/>
- <delta pt="1" x="-2" y="9"/>
- <delta pt="2" x="4" y="9"/>
- <delta pt="3" x="4" y="9"/>
- <delta pt="4" x="2" y="9"/>
- <delta pt="5" x="2" y="0"/>
- <delta pt="6" x="-2" y="0"/>
- <delta pt="7" x="4" y="0"/>
- <delta pt="8" x="4" y="0"/>
- <delta pt="9" x="4" y="0"/>
- <delta pt="10" x="4" y="0"/>
- <delta pt="11" x="6" y="-8"/>
- <delta pt="12" x="10" y="0"/>
- <delta pt="13" x="2" y="0"/>
- <delta pt="14" x="0" y="-5"/>
- <delta pt="15" x="3" y="5"/>
- <delta pt="16" x="-5" y="5"/>
- <delta pt="17" x="1" y="-5"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="0" y="8"/>
- <delta pt="20" x="-4" y="8"/>
- <delta pt="21" x="0" y="8"/>
- <delta pt="22" x="0" y="8"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="3" y="0"/>
- <delta pt="1" x="3" y="-15"/>
- <delta pt="2" x="-6" y="-15"/>
- <delta pt="3" x="-6" y="-15"/>
- <delta pt="4" x="-3" y="-15"/>
- <delta pt="5" x="-3" y="0"/>
- <delta pt="6" x="3" y="0"/>
- <delta pt="7" x="-6" y="0"/>
- <delta pt="8" x="-6" y="0"/>
- <delta pt="9" x="-6" y="0"/>
- <delta pt="10" x="-6" y="0"/>
- <delta pt="11" x="-11" y="13"/>
- <delta pt="12" x="-17" y="0"/>
- <delta pt="13" x="-3" y="0"/>
- <delta pt="14" x="-1" y="8"/>
- <delta pt="15" x="-5" y="-9"/>
- <delta pt="16" x="8" y="-9"/>
- <delta pt="17" x="-1" y="8"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="0" y="-13"/>
- <delta pt="20" x="6" y="-13"/>
- <delta pt="21" x="0" y="-13"/>
- <delta pt="22" x="0" y="-13"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni0061">
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <delta pt="0" x="11" y="-8"/>
- <delta pt="1" x="11" y="4"/>
- <delta pt="2" x="22" y="5"/>
- <delta pt="3" x="-4" y="-8"/>
- <delta pt="4" x="6" y="-5"/>
- <delta pt="5" x="3" y="-11"/>
- <delta pt="6" x="4" y="-9"/>
- <delta pt="7" x="4" y="9"/>
- <delta pt="8" x="0" y="7"/>
- <delta pt="9" x="-9" y="8"/>
- <delta pt="10" x="-24" y="3"/>
- <delta pt="11" x="-18" y="6"/>
- <delta pt="12" x="-44" y="1"/>
- <delta pt="13" x="-44" y="-16"/>
- <delta pt="14" x="-44" y="-22"/>
- <delta pt="15" x="-36" y="-39"/>
- <delta pt="16" x="-24" y="-39"/>
- <delta pt="17" x="-7" y="-39"/>
- <delta pt="18" x="26" y="-15"/>
- <delta pt="19" x="26" y="3"/>
- <delta pt="20" x="17" y="0"/>
- <delta pt="21" x="3" y="-4"/>
- <delta pt="22" x="23" y="15"/>
- <delta pt="23" x="22" y="8"/>
- <delta pt="24" x="6" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="2" y="0"/>
- <delta pt="27" x="11" y="-2"/>
- <delta pt="28" x="30" y="7"/>
- <delta pt="29" x="30" y="4"/>
- <delta pt="30" x="30" y="13"/>
- <delta pt="31" x="14" y="21"/>
- <delta pt="32" x="3" y="21"/>
- <delta pt="33" x="-15" y="21"/>
- <delta pt="34" x="-32" y="5"/>
- <delta pt="35" x="-34" y="-9"/>
- <delta pt="36" x="-48" y="-14"/>
- <delta pt="37" x="-40" y="4"/>
- <delta pt="38" x="-36" y="14"/>
- <delta pt="39" x="-24" y="27"/>
- <delta pt="40" x="-13" y="27"/>
- <delta pt="41" x="12" y="27"/>
- <delta pt="42" x="10" y="6"/>
- <delta pt="43" x="12" y="5"/>
- <delta pt="44" x="-4" y="-4"/>
- <delta pt="45" x="-16" y="-4"/>
- <delta pt="46" x="-20" y="-4"/>
- <delta pt="47" x="-22" y="7"/>
- <delta pt="48" x="-22" y="25"/>
- <delta pt="49" x="-22" y="10"/>
- <delta pt="50" x="-22" y="-15"/>
- <delta pt="51" x="-16" y="-30"/>
- <delta pt="52" x="-9" y="-30"/>
- <delta pt="53" x="-12" y="-30"/>
- <delta pt="54" x="-11" y="-35"/>
- <delta pt="55" x="-5" y="-35"/>
- <delta pt="56" x="-15" y="-27"/>
- <delta pt="57" x="-10" y="-3"/>
- <delta pt="58" x="9" y="-3"/>
- <delta pt="59" x="14" y="-3"/>
- <delta pt="60" x="33" y="-1"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="-3" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-21" y="1"/>
- <delta pt="1" x="-21" y="17"/>
- <delta pt="2" x="-2" y="28"/>
- <delta pt="3" x="20" y="23"/>
- <delta pt="4" x="19" y="20"/>
- <delta pt="5" x="28" y="21"/>
- <delta pt="6" x="26" y="23"/>
- <delta pt="7" x="26" y="15"/>
- <delta pt="8" x="24" y="12"/>
- <delta pt="9" x="30" y="17"/>
- <delta pt="10" x="31" y="15"/>
- <delta pt="11" x="77" y="31"/>
- <delta pt="12" x="66" y="36"/>
- <delta pt="13" x="66" y="18"/>
- <delta pt="14" x="66" y="21"/>
- <delta pt="15" x="49" y="19"/>
- <delta pt="16" x="37" y="19"/>
- <delta pt="17" x="21" y="19"/>
- <delta pt="18" x="-2" y="5"/>
- <delta pt="19" x="-34" y="-18"/>
- <delta pt="20" x="-6" y="3"/>
- <delta pt="21" x="-11" y="12"/>
- <delta pt="22" x="-29" y="-11"/>
- <delta pt="23" x="-17" y="-2"/>
- <delta pt="24" x="-13" y="-3"/>
- <delta pt="25" x="-25" y="-3"/>
- <delta pt="26" x="-29" y="-3"/>
- <delta pt="27" x="-21" y="2"/>
- <delta pt="28" x="-34" y="-14"/>
- <delta pt="29" x="-34" y="17"/>
- <delta pt="30" x="-34" y="7"/>
- <delta pt="31" x="-18" y="7"/>
- <delta pt="32" x="-16" y="7"/>
- <delta pt="33" x="-18" y="7"/>
- <delta pt="34" x="-15" y="9"/>
- <delta pt="35" x="-21" y="12"/>
- <delta pt="36" x="19" y="23"/>
- <delta pt="37" x="45" y="46"/>
- <delta pt="38" x="52" y="7"/>
- <delta pt="39" x="26" y="-21"/>
- <delta pt="40" x="14" y="-21"/>
- <delta pt="41" x="-5" y="-21"/>
- <delta pt="42" x="-17" y="-7"/>
- <delta pt="43" x="-31" y="1"/>
- <delta pt="44" x="-12" y="16"/>
- <delta pt="45" x="34" y="16"/>
- <delta pt="46" x="61" y="16"/>
- <delta pt="47" x="70" y="4"/>
- <delta pt="48" x="70" y="-5"/>
- <delta pt="49" x="70" y="-22"/>
- <delta pt="50" x="70" y="4"/>
- <delta pt="51" x="59" y="22"/>
- <delta pt="52" x="50" y="22"/>
- <delta pt="53" x="43" y="22"/>
- <delta pt="54" x="37" y="19"/>
- <delta pt="55" x="38" y="22"/>
- <delta pt="56" x="47" y="28"/>
- <delta pt="57" x="46" y="-6"/>
- <delta pt="58" x="-2" y="-6"/>
- <delta pt="59" x="-16" y="-6"/>
- <delta pt="60" x="-25" y="-13"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="32" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="0" y="-3"/>
- <delta pt="1" x="0" y="-1"/>
- <delta pt="2" x="0" y="-3"/>
- <delta pt="3" x="0" y="-3"/>
- <delta pt="4" x="0" y="-3"/>
- <delta pt="5" x="0" y="-3"/>
- <delta pt="6" x="0" y="-3"/>
- <delta pt="7" x="0" y="4"/>
- <delta pt="8" x="0" y="4"/>
- <delta pt="9" x="2" y="5"/>
- <delta pt="10" x="6" y="7"/>
- <delta pt="11" x="1" y="5"/>
- <delta pt="12" x="0" y="-1"/>
- <delta pt="13" x="0" y="-6"/>
- <delta pt="14" x="0" y="-6"/>
- <delta pt="15" x="-1" y="-6"/>
- <delta pt="16" x="0" y="-6"/>
- <delta pt="17" x="0" y="-6"/>
- <delta pt="18" x="0" y="-5"/>
- <delta pt="19" x="0" y="-4"/>
- <delta pt="20" x="0" y="-1"/>
- <delta pt="21" x="0" y="0"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="-1"/>
- <delta pt="28" x="0" y="-2"/>
- <delta pt="29" x="0" y="7"/>
- <delta pt="30" x="0" y="6"/>
- <delta pt="31" x="0" y="7"/>
- <delta pt="32" x="0" y="7"/>
- <delta pt="33" x="0" y="7"/>
- <delta pt="34" x="0" y="7"/>
- <delta pt="35" x="0" y="7"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="0" y="0"/>
- <delta pt="38" x="0" y="0"/>
- <delta pt="39" x="0" y="0"/>
- <delta pt="40" x="0" y="0"/>
- <delta pt="41" x="0" y="0"/>
- <delta pt="42" x="0" y="0"/>
- <delta pt="43" x="0" y="0"/>
- <delta pt="44" x="0" y="0"/>
- <delta pt="45" x="0" y="0"/>
- <delta pt="46" x="0" y="0"/>
- <delta pt="47" x="0" y="0"/>
- <delta pt="48" x="0" y="0"/>
- <delta pt="49" x="0" y="-6"/>
- <delta pt="50" x="0" y="-7"/>
- <delta pt="51" x="0" y="-8"/>
- <delta pt="52" x="0" y="-8"/>
- <delta pt="53" x="1" y="-8"/>
- <delta pt="54" x="2" y="-5"/>
- <delta pt="55" x="4" y="-2"/>
- <delta pt="56" x="0" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="0" y="0"/>
- <delta pt="59" x="0" y="0"/>
- <delta pt="60" x="0" y="-1"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="0" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="0" y="3"/>
- <delta pt="1" x="0" y="1"/>
- <delta pt="2" x="0" y="3"/>
- <delta pt="3" x="0" y="3"/>
- <delta pt="4" x="0" y="3"/>
- <delta pt="5" x="0" y="3"/>
- <delta pt="6" x="0" y="3"/>
- <delta pt="7" x="0" y="-4"/>
- <delta pt="8" x="0" y="-4"/>
- <delta pt="9" x="-2" y="-5"/>
- <delta pt="10" x="-6" y="-7"/>
- <delta pt="11" x="-1" y="-5"/>
- <delta pt="12" x="0" y="1"/>
- <delta pt="13" x="0" y="6"/>
- <delta pt="14" x="0" y="6"/>
- <delta pt="15" x="1" y="6"/>
- <delta pt="16" x="0" y="6"/>
- <delta pt="17" x="0" y="6"/>
- <delta pt="18" x="0" y="5"/>
- <delta pt="19" x="0" y="4"/>
- <delta pt="20" x="0" y="1"/>
- <delta pt="21" x="0" y="0"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="1"/>
- <delta pt="28" x="0" y="2"/>
- <delta pt="29" x="0" y="-7"/>
- <delta pt="30" x="0" y="-6"/>
- <delta pt="31" x="0" y="-7"/>
- <delta pt="32" x="0" y="-7"/>
- <delta pt="33" x="0" y="-7"/>
- <delta pt="34" x="0" y="-7"/>
- <delta pt="35" x="0" y="-7"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="0" y="0"/>
- <delta pt="38" x="0" y="0"/>
- <delta pt="39" x="0" y="0"/>
- <delta pt="40" x="0" y="0"/>
- <delta pt="41" x="0" y="0"/>
- <delta pt="42" x="0" y="0"/>
- <delta pt="43" x="0" y="0"/>
- <delta pt="44" x="0" y="0"/>
- <delta pt="45" x="0" y="0"/>
- <delta pt="46" x="0" y="0"/>
- <delta pt="47" x="0" y="0"/>
- <delta pt="48" x="0" y="0"/>
- <delta pt="49" x="0" y="6"/>
- <delta pt="50" x="0" y="7"/>
- <delta pt="51" x="0" y="8"/>
- <delta pt="52" x="0" y="8"/>
- <delta pt="53" x="-1" y="8"/>
- <delta pt="54" x="-2" y="5"/>
- <delta pt="55" x="-4" y="2"/>
- <delta pt="56" x="0" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="0" y="0"/>
- <delta pt="59" x="0" y="0"/>
- <delta pt="60" x="0" y="1"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="0" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="0" y="-5"/>
- <delta pt="1" x="0" y="0"/>
- <delta pt="2" x="3" y="-4"/>
- <delta pt="3" x="0" y="-4"/>
- <delta pt="4" x="0" y="-4"/>
- <delta pt="5" x="0" y="-4"/>
- <delta pt="6" x="0" y="-4"/>
- <delta pt="7" x="0" y="8"/>
- <delta pt="8" x="0" y="8"/>
- <delta pt="9" x="5" y="9"/>
- <delta pt="10" x="11" y="13"/>
- <delta pt="11" x="2" y="10"/>
- <delta pt="12" x="0" y="0"/>
- <delta pt="13" x="0" y="-9"/>
- <delta pt="14" x="0" y="-9"/>
- <delta pt="15" x="-1" y="-9"/>
- <delta pt="16" x="0" y="-9"/>
- <delta pt="17" x="0" y="-9"/>
- <delta pt="18" x="0" y="-10"/>
- <delta pt="19" x="0" y="-8"/>
- <delta pt="20" x="0" y="-2"/>
- <delta pt="21" x="0" y="1"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="1" y="-1"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="-1"/>
- <delta pt="28" x="0" y="-4"/>
- <delta pt="29" x="0" y="12"/>
- <delta pt="30" x="0" y="13"/>
- <delta pt="31" x="0" y="13"/>
- <delta pt="32" x="0" y="13"/>
- <delta pt="33" x="0" y="13"/>
- <delta pt="34" x="0" y="13"/>
- <delta pt="35" x="0" y="13"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="0" y="0"/>
- <delta pt="38" x="0" y="0"/>
- <delta pt="39" x="0" y="1"/>
- <delta pt="40" x="0" y="1"/>
- <delta pt="41" x="0" y="1"/>
- <delta pt="42" x="0" y="1"/>
- <delta pt="43" x="0" y="0"/>
- <delta pt="44" x="0" y="0"/>
- <delta pt="45" x="0" y="0"/>
- <delta pt="46" x="0" y="0"/>
- <delta pt="47" x="0" y="-1"/>
- <delta pt="48" x="0" y="-1"/>
- <delta pt="49" x="0" y="-9"/>
- <delta pt="50" x="0" y="-13"/>
- <delta pt="51" x="1" y="-14"/>
- <delta pt="52" x="1" y="-14"/>
- <delta pt="53" x="2" y="-14"/>
- <delta pt="54" x="5" y="-11"/>
- <delta pt="55" x="7" y="-4"/>
- <delta pt="56" x="0" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="0" y="0"/>
- <delta pt="59" x="0" y="0"/>
- <delta pt="60" x="1" y="0"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="0" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- </glyphVariations>
<glyphVariations glyph="uni0024">
<tuple>
<coord axis="wght" value="-1.0"/>
@@ -1606,6 +1103,509 @@
<delta pt="65" x="0" y="0"/>
</tuple>
</glyphVariations>
+ <glyphVariations glyph="uni0041">
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <delta pt="0" x="7" y="0"/>
+ <delta pt="1" x="7" y="-20"/>
+ <delta pt="2" x="-6" y="-29"/>
+ <delta pt="3" x="-12" y="-29"/>
+ <delta pt="4" x="-25" y="-20"/>
+ <delta pt="5" x="-25" y="0"/>
+ <delta pt="6" x="14" y="0"/>
+ <delta pt="7" x="4" y="9"/>
+ <delta pt="8" x="-36" y="9"/>
+ <delta pt="9" x="-37" y="0"/>
+ <delta pt="10" x="24" y="0"/>
+ <delta pt="11" x="9" y="58"/>
+ <delta pt="12" x="3" y="68"/>
+ <delta pt="13" x="-4" y="0"/>
+ <delta pt="14" x="3" y="28"/>
+ <delta pt="15" x="-4" y="2"/>
+ <delta pt="16" x="4" y="2"/>
+ <delta pt="17" x="-4" y="28"/>
+ <delta pt="18" x="20" y="0"/>
+ <delta pt="19" x="20" y="-20"/>
+ <delta pt="20" x="14" y="-29"/>
+ <delta pt="21" x="8" y="-29"/>
+ <delta pt="22" x="-2" y="-20"/>
+ <delta pt="23" x="-2" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="-10" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="5" y="0"/>
+ <delta pt="1" x="5" y="19"/>
+ <delta pt="2" x="9" y="19"/>
+ <delta pt="3" x="6" y="19"/>
+ <delta pt="4" x="-15" y="19"/>
+ <delta pt="5" x="-15" y="0"/>
+ <delta pt="6" x="-6" y="0"/>
+ <delta pt="7" x="-14" y="-23"/>
+ <delta pt="8" x="46" y="-23"/>
+ <delta pt="9" x="39" y="0"/>
+ <delta pt="10" x="-69" y="0"/>
+ <delta pt="11" x="-27" y="-86"/>
+ <delta pt="12" x="-7" y="-16"/>
+ <delta pt="13" x="11" y="0"/>
+ <delta pt="14" x="-2" y="-39"/>
+ <delta pt="15" x="-1" y="-22"/>
+ <delta pt="16" x="-1" y="-22"/>
+ <delta pt="17" x="8" y="-39"/>
+ <delta pt="18" x="-41" y="0"/>
+ <delta pt="19" x="-41" y="16"/>
+ <delta pt="20" x="-59" y="16"/>
+ <delta pt="21" x="6" y="16"/>
+ <delta pt="22" x="12" y="16"/>
+ <delta pt="23" x="12" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="17" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="2" y="0"/>
+ <delta pt="1" x="2" y="-9"/>
+ <delta pt="2" x="-4" y="-9"/>
+ <delta pt="3" x="-4" y="-9"/>
+ <delta pt="4" x="-2" y="-9"/>
+ <delta pt="5" x="-2" y="0"/>
+ <delta pt="6" x="2" y="0"/>
+ <delta pt="7" x="-4" y="0"/>
+ <delta pt="8" x="-4" y="0"/>
+ <delta pt="9" x="-4" y="0"/>
+ <delta pt="10" x="-4" y="0"/>
+ <delta pt="11" x="-6" y="8"/>
+ <delta pt="12" x="-10" y="0"/>
+ <delta pt="13" x="-2" y="0"/>
+ <delta pt="14" x="0" y="5"/>
+ <delta pt="15" x="-3" y="-5"/>
+ <delta pt="16" x="5" y="-5"/>
+ <delta pt="17" x="-1" y="5"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="-8"/>
+ <delta pt="20" x="4" y="-8"/>
+ <delta pt="21" x="0" y="-8"/>
+ <delta pt="22" x="0" y="-8"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="-2" y="9"/>
+ <delta pt="2" x="4" y="9"/>
+ <delta pt="3" x="4" y="9"/>
+ <delta pt="4" x="2" y="9"/>
+ <delta pt="5" x="2" y="0"/>
+ <delta pt="6" x="-2" y="0"/>
+ <delta pt="7" x="4" y="0"/>
+ <delta pt="8" x="4" y="0"/>
+ <delta pt="9" x="4" y="0"/>
+ <delta pt="10" x="4" y="0"/>
+ <delta pt="11" x="6" y="-8"/>
+ <delta pt="12" x="10" y="0"/>
+ <delta pt="13" x="2" y="0"/>
+ <delta pt="14" x="0" y="-5"/>
+ <delta pt="15" x="3" y="5"/>
+ <delta pt="16" x="-5" y="5"/>
+ <delta pt="17" x="1" y="-5"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="8"/>
+ <delta pt="20" x="-4" y="8"/>
+ <delta pt="21" x="0" y="8"/>
+ <delta pt="22" x="0" y="8"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="3" y="0"/>
+ <delta pt="1" x="3" y="-15"/>
+ <delta pt="2" x="-6" y="-15"/>
+ <delta pt="3" x="-6" y="-15"/>
+ <delta pt="4" x="-3" y="-15"/>
+ <delta pt="5" x="-3" y="0"/>
+ <delta pt="6" x="3" y="0"/>
+ <delta pt="7" x="-6" y="0"/>
+ <delta pt="8" x="-6" y="0"/>
+ <delta pt="9" x="-6" y="0"/>
+ <delta pt="10" x="-6" y="0"/>
+ <delta pt="11" x="-11" y="13"/>
+ <delta pt="12" x="-17" y="0"/>
+ <delta pt="13" x="-3" y="0"/>
+ <delta pt="14" x="-1" y="8"/>
+ <delta pt="15" x="-5" y="-9"/>
+ <delta pt="16" x="8" y="-9"/>
+ <delta pt="17" x="-1" y="8"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="-13"/>
+ <delta pt="20" x="6" y="-13"/>
+ <delta pt="21" x="0" y="-13"/>
+ <delta pt="22" x="0" y="-13"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="uni0061">
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <delta pt="0" x="11" y="-8"/>
+ <delta pt="1" x="11" y="4"/>
+ <delta pt="2" x="22" y="5"/>
+ <delta pt="3" x="-4" y="-8"/>
+ <delta pt="4" x="6" y="-5"/>
+ <delta pt="5" x="3" y="-11"/>
+ <delta pt="6" x="4" y="-9"/>
+ <delta pt="7" x="4" y="9"/>
+ <delta pt="8" x="0" y="7"/>
+ <delta pt="9" x="-9" y="8"/>
+ <delta pt="10" x="-24" y="3"/>
+ <delta pt="11" x="-18" y="6"/>
+ <delta pt="12" x="-44" y="1"/>
+ <delta pt="13" x="-44" y="-16"/>
+ <delta pt="14" x="-44" y="-22"/>
+ <delta pt="15" x="-36" y="-39"/>
+ <delta pt="16" x="-24" y="-39"/>
+ <delta pt="17" x="-7" y="-39"/>
+ <delta pt="18" x="26" y="-15"/>
+ <delta pt="19" x="26" y="3"/>
+ <delta pt="20" x="17" y="0"/>
+ <delta pt="21" x="3" y="-4"/>
+ <delta pt="22" x="23" y="15"/>
+ <delta pt="23" x="22" y="8"/>
+ <delta pt="24" x="6" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="2" y="0"/>
+ <delta pt="27" x="11" y="-2"/>
+ <delta pt="28" x="30" y="7"/>
+ <delta pt="29" x="30" y="4"/>
+ <delta pt="30" x="30" y="13"/>
+ <delta pt="31" x="14" y="21"/>
+ <delta pt="32" x="3" y="21"/>
+ <delta pt="33" x="-15" y="21"/>
+ <delta pt="34" x="-32" y="5"/>
+ <delta pt="35" x="-34" y="-9"/>
+ <delta pt="36" x="-48" y="-14"/>
+ <delta pt="37" x="-40" y="4"/>
+ <delta pt="38" x="-36" y="14"/>
+ <delta pt="39" x="-24" y="27"/>
+ <delta pt="40" x="-13" y="27"/>
+ <delta pt="41" x="12" y="27"/>
+ <delta pt="42" x="10" y="6"/>
+ <delta pt="43" x="12" y="5"/>
+ <delta pt="44" x="-4" y="-4"/>
+ <delta pt="45" x="-16" y="-4"/>
+ <delta pt="46" x="-20" y="-4"/>
+ <delta pt="47" x="-22" y="7"/>
+ <delta pt="48" x="-22" y="25"/>
+ <delta pt="49" x="-22" y="10"/>
+ <delta pt="50" x="-22" y="-15"/>
+ <delta pt="51" x="-16" y="-30"/>
+ <delta pt="52" x="-9" y="-30"/>
+ <delta pt="53" x="-12" y="-30"/>
+ <delta pt="54" x="-11" y="-35"/>
+ <delta pt="55" x="-5" y="-35"/>
+ <delta pt="56" x="-15" y="-27"/>
+ <delta pt="57" x="-10" y="-3"/>
+ <delta pt="58" x="9" y="-3"/>
+ <delta pt="59" x="14" y="-3"/>
+ <delta pt="60" x="33" y="-1"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="-3" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="-21" y="1"/>
+ <delta pt="1" x="-21" y="17"/>
+ <delta pt="2" x="-2" y="28"/>
+ <delta pt="3" x="20" y="23"/>
+ <delta pt="4" x="19" y="20"/>
+ <delta pt="5" x="28" y="21"/>
+ <delta pt="6" x="26" y="23"/>
+ <delta pt="7" x="26" y="15"/>
+ <delta pt="8" x="24" y="12"/>
+ <delta pt="9" x="30" y="17"/>
+ <delta pt="10" x="31" y="15"/>
+ <delta pt="11" x="77" y="31"/>
+ <delta pt="12" x="66" y="36"/>
+ <delta pt="13" x="66" y="18"/>
+ <delta pt="14" x="66" y="21"/>
+ <delta pt="15" x="49" y="19"/>
+ <delta pt="16" x="37" y="19"/>
+ <delta pt="17" x="21" y="19"/>
+ <delta pt="18" x="-2" y="5"/>
+ <delta pt="19" x="-34" y="-18"/>
+ <delta pt="20" x="-6" y="3"/>
+ <delta pt="21" x="-11" y="12"/>
+ <delta pt="22" x="-29" y="-11"/>
+ <delta pt="23" x="-17" y="-2"/>
+ <delta pt="24" x="-13" y="-3"/>
+ <delta pt="25" x="-25" y="-3"/>
+ <delta pt="26" x="-29" y="-3"/>
+ <delta pt="27" x="-21" y="2"/>
+ <delta pt="28" x="-34" y="-14"/>
+ <delta pt="29" x="-34" y="17"/>
+ <delta pt="30" x="-34" y="7"/>
+ <delta pt="31" x="-18" y="7"/>
+ <delta pt="32" x="-16" y="7"/>
+ <delta pt="33" x="-18" y="7"/>
+ <delta pt="34" x="-15" y="9"/>
+ <delta pt="35" x="-21" y="12"/>
+ <delta pt="36" x="19" y="23"/>
+ <delta pt="37" x="45" y="46"/>
+ <delta pt="38" x="52" y="7"/>
+ <delta pt="39" x="26" y="-21"/>
+ <delta pt="40" x="14" y="-21"/>
+ <delta pt="41" x="-5" y="-21"/>
+ <delta pt="42" x="-17" y="-7"/>
+ <delta pt="43" x="-31" y="1"/>
+ <delta pt="44" x="-12" y="16"/>
+ <delta pt="45" x="34" y="16"/>
+ <delta pt="46" x="61" y="16"/>
+ <delta pt="47" x="70" y="4"/>
+ <delta pt="48" x="70" y="-5"/>
+ <delta pt="49" x="70" y="-22"/>
+ <delta pt="50" x="70" y="4"/>
+ <delta pt="51" x="59" y="22"/>
+ <delta pt="52" x="50" y="22"/>
+ <delta pt="53" x="43" y="22"/>
+ <delta pt="54" x="37" y="19"/>
+ <delta pt="55" x="38" y="22"/>
+ <delta pt="56" x="47" y="28"/>
+ <delta pt="57" x="46" y="-6"/>
+ <delta pt="58" x="-2" y="-6"/>
+ <delta pt="59" x="-16" y="-6"/>
+ <delta pt="60" x="-25" y="-13"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="32" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="0" y="-3"/>
+ <delta pt="1" x="0" y="-1"/>
+ <delta pt="2" x="0" y="-3"/>
+ <delta pt="3" x="0" y="-3"/>
+ <delta pt="4" x="0" y="-3"/>
+ <delta pt="5" x="0" y="-3"/>
+ <delta pt="6" x="0" y="-3"/>
+ <delta pt="7" x="0" y="4"/>
+ <delta pt="8" x="0" y="4"/>
+ <delta pt="9" x="2" y="5"/>
+ <delta pt="10" x="6" y="7"/>
+ <delta pt="11" x="1" y="5"/>
+ <delta pt="12" x="0" y="-1"/>
+ <delta pt="13" x="0" y="-6"/>
+ <delta pt="14" x="0" y="-6"/>
+ <delta pt="15" x="-1" y="-6"/>
+ <delta pt="16" x="0" y="-6"/>
+ <delta pt="17" x="0" y="-6"/>
+ <delta pt="18" x="0" y="-5"/>
+ <delta pt="19" x="0" y="-4"/>
+ <delta pt="20" x="0" y="-1"/>
+ <delta pt="21" x="0" y="0"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="-1"/>
+ <delta pt="28" x="0" y="-2"/>
+ <delta pt="29" x="0" y="7"/>
+ <delta pt="30" x="0" y="6"/>
+ <delta pt="31" x="0" y="7"/>
+ <delta pt="32" x="0" y="7"/>
+ <delta pt="33" x="0" y="7"/>
+ <delta pt="34" x="0" y="7"/>
+ <delta pt="35" x="0" y="7"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="0"/>
+ <delta pt="40" x="0" y="0"/>
+ <delta pt="41" x="0" y="0"/>
+ <delta pt="42" x="0" y="0"/>
+ <delta pt="43" x="0" y="0"/>
+ <delta pt="44" x="0" y="0"/>
+ <delta pt="45" x="0" y="0"/>
+ <delta pt="46" x="0" y="0"/>
+ <delta pt="47" x="0" y="0"/>
+ <delta pt="48" x="0" y="0"/>
+ <delta pt="49" x="0" y="-6"/>
+ <delta pt="50" x="0" y="-7"/>
+ <delta pt="51" x="0" y="-8"/>
+ <delta pt="52" x="0" y="-8"/>
+ <delta pt="53" x="1" y="-8"/>
+ <delta pt="54" x="2" y="-5"/>
+ <delta pt="55" x="4" y="-2"/>
+ <delta pt="56" x="0" y="0"/>
+ <delta pt="57" x="0" y="0"/>
+ <delta pt="58" x="0" y="0"/>
+ <delta pt="59" x="0" y="0"/>
+ <delta pt="60" x="0" y="-1"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="0" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="0" y="3"/>
+ <delta pt="1" x="0" y="1"/>
+ <delta pt="2" x="0" y="3"/>
+ <delta pt="3" x="0" y="3"/>
+ <delta pt="4" x="0" y="3"/>
+ <delta pt="5" x="0" y="3"/>
+ <delta pt="6" x="0" y="3"/>
+ <delta pt="7" x="0" y="-4"/>
+ <delta pt="8" x="0" y="-4"/>
+ <delta pt="9" x="-2" y="-5"/>
+ <delta pt="10" x="-6" y="-7"/>
+ <delta pt="11" x="-1" y="-5"/>
+ <delta pt="12" x="0" y="1"/>
+ <delta pt="13" x="0" y="6"/>
+ <delta pt="14" x="0" y="6"/>
+ <delta pt="15" x="1" y="6"/>
+ <delta pt="16" x="0" y="6"/>
+ <delta pt="17" x="0" y="6"/>
+ <delta pt="18" x="0" y="5"/>
+ <delta pt="19" x="0" y="4"/>
+ <delta pt="20" x="0" y="1"/>
+ <delta pt="21" x="0" y="0"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="1"/>
+ <delta pt="28" x="0" y="2"/>
+ <delta pt="29" x="0" y="-7"/>
+ <delta pt="30" x="0" y="-6"/>
+ <delta pt="31" x="0" y="-7"/>
+ <delta pt="32" x="0" y="-7"/>
+ <delta pt="33" x="0" y="-7"/>
+ <delta pt="34" x="0" y="-7"/>
+ <delta pt="35" x="0" y="-7"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="0"/>
+ <delta pt="40" x="0" y="0"/>
+ <delta pt="41" x="0" y="0"/>
+ <delta pt="42" x="0" y="0"/>
+ <delta pt="43" x="0" y="0"/>
+ <delta pt="44" x="0" y="0"/>
+ <delta pt="45" x="0" y="0"/>
+ <delta pt="46" x="0" y="0"/>
+ <delta pt="47" x="0" y="0"/>
+ <delta pt="48" x="0" y="0"/>
+ <delta pt="49" x="0" y="6"/>
+ <delta pt="50" x="0" y="7"/>
+ <delta pt="51" x="0" y="8"/>
+ <delta pt="52" x="0" y="8"/>
+ <delta pt="53" x="-1" y="8"/>
+ <delta pt="54" x="-2" y="5"/>
+ <delta pt="55" x="-4" y="2"/>
+ <delta pt="56" x="0" y="0"/>
+ <delta pt="57" x="0" y="0"/>
+ <delta pt="58" x="0" y="0"/>
+ <delta pt="59" x="0" y="0"/>
+ <delta pt="60" x="0" y="1"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="0" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="0" y="-5"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="3" y="-4"/>
+ <delta pt="3" x="0" y="-4"/>
+ <delta pt="4" x="0" y="-4"/>
+ <delta pt="5" x="0" y="-4"/>
+ <delta pt="6" x="0" y="-4"/>
+ <delta pt="7" x="0" y="8"/>
+ <delta pt="8" x="0" y="8"/>
+ <delta pt="9" x="5" y="9"/>
+ <delta pt="10" x="11" y="13"/>
+ <delta pt="11" x="2" y="10"/>
+ <delta pt="12" x="0" y="0"/>
+ <delta pt="13" x="0" y="-9"/>
+ <delta pt="14" x="0" y="-9"/>
+ <delta pt="15" x="-1" y="-9"/>
+ <delta pt="16" x="0" y="-9"/>
+ <delta pt="17" x="0" y="-9"/>
+ <delta pt="18" x="0" y="-10"/>
+ <delta pt="19" x="0" y="-8"/>
+ <delta pt="20" x="0" y="-2"/>
+ <delta pt="21" x="0" y="1"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="1" y="-1"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="-1"/>
+ <delta pt="28" x="0" y="-4"/>
+ <delta pt="29" x="0" y="12"/>
+ <delta pt="30" x="0" y="13"/>
+ <delta pt="31" x="0" y="13"/>
+ <delta pt="32" x="0" y="13"/>
+ <delta pt="33" x="0" y="13"/>
+ <delta pt="34" x="0" y="13"/>
+ <delta pt="35" x="0" y="13"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="1"/>
+ <delta pt="40" x="0" y="1"/>
+ <delta pt="41" x="0" y="1"/>
+ <delta pt="42" x="0" y="1"/>
+ <delta pt="43" x="0" y="0"/>
+ <delta pt="44" x="0" y="0"/>
+ <delta pt="45" x="0" y="0"/>
+ <delta pt="46" x="0" y="0"/>
+ <delta pt="47" x="0" y="-1"/>
+ <delta pt="48" x="0" y="-1"/>
+ <delta pt="49" x="0" y="-9"/>
+ <delta pt="50" x="0" y="-13"/>
+ <delta pt="51" x="1" y="-14"/>
+ <delta pt="52" x="1" y="-14"/>
+ <delta pt="53" x="2" y="-14"/>
+ <delta pt="54" x="5" y="-11"/>
+ <delta pt="55" x="7" y="-4"/>
+ <delta pt="56" x="0" y="0"/>
+ <delta pt="57" x="0" y="0"/>
+ <delta pt="58" x="0" y="0"/>
+ <delta pt="59" x="0" y="0"/>
+ <delta pt="60" x="1" y="0"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="0" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
</gvar>
</ttFont>
diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx
index 7e5d9561..20add49e 100644
--- a/Tests/varLib/data/test_results/BuildMain.ttx
+++ b/Tests/varLib/data/test_results/BuildMain.ttx
@@ -1051,509 +1051,6 @@
<delta pt="3" x="0" y="0"/>
</tuple>
</glyphVariations>
- <glyphVariations glyph="uni0041">
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <delta pt="0" x="7" y="0"/>
- <delta pt="1" x="7" y="-20"/>
- <delta pt="2" x="-6" y="-29"/>
- <delta pt="3" x="-12" y="-29"/>
- <delta pt="4" x="-25" y="-20"/>
- <delta pt="5" x="-25" y="0"/>
- <delta pt="6" x="14" y="0"/>
- <delta pt="7" x="4" y="9"/>
- <delta pt="8" x="-36" y="9"/>
- <delta pt="9" x="-37" y="0"/>
- <delta pt="10" x="24" y="0"/>
- <delta pt="11" x="9" y="58"/>
- <delta pt="12" x="3" y="68"/>
- <delta pt="13" x="-4" y="0"/>
- <delta pt="14" x="3" y="28"/>
- <delta pt="15" x="-4" y="2"/>
- <delta pt="16" x="4" y="2"/>
- <delta pt="17" x="-4" y="28"/>
- <delta pt="18" x="20" y="0"/>
- <delta pt="19" x="20" y="-20"/>
- <delta pt="20" x="14" y="-29"/>
- <delta pt="21" x="8" y="-29"/>
- <delta pt="22" x="-2" y="-20"/>
- <delta pt="23" x="-2" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="-10" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="5" y="0"/>
- <delta pt="1" x="5" y="19"/>
- <delta pt="2" x="9" y="19"/>
- <delta pt="3" x="6" y="19"/>
- <delta pt="4" x="-15" y="19"/>
- <delta pt="5" x="-15" y="0"/>
- <delta pt="6" x="-6" y="0"/>
- <delta pt="7" x="-14" y="-23"/>
- <delta pt="8" x="46" y="-23"/>
- <delta pt="9" x="39" y="0"/>
- <delta pt="10" x="-69" y="0"/>
- <delta pt="11" x="-27" y="-86"/>
- <delta pt="12" x="-7" y="-16"/>
- <delta pt="13" x="11" y="0"/>
- <delta pt="14" x="-2" y="-39"/>
- <delta pt="15" x="-1" y="-22"/>
- <delta pt="16" x="-1" y="-22"/>
- <delta pt="17" x="8" y="-39"/>
- <delta pt="18" x="-41" y="0"/>
- <delta pt="19" x="-41" y="16"/>
- <delta pt="20" x="-59" y="16"/>
- <delta pt="21" x="6" y="16"/>
- <delta pt="22" x="12" y="16"/>
- <delta pt="23" x="12" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="17" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="2" y="0"/>
- <delta pt="1" x="2" y="-9"/>
- <delta pt="2" x="-4" y="-9"/>
- <delta pt="3" x="-4" y="-9"/>
- <delta pt="4" x="-2" y="-9"/>
- <delta pt="5" x="-2" y="0"/>
- <delta pt="6" x="2" y="0"/>
- <delta pt="7" x="-4" y="0"/>
- <delta pt="8" x="-4" y="0"/>
- <delta pt="9" x="-4" y="0"/>
- <delta pt="10" x="-4" y="0"/>
- <delta pt="11" x="-6" y="8"/>
- <delta pt="12" x="-10" y="0"/>
- <delta pt="13" x="-2" y="0"/>
- <delta pt="14" x="0" y="5"/>
- <delta pt="15" x="-3" y="-5"/>
- <delta pt="16" x="5" y="-5"/>
- <delta pt="17" x="-1" y="5"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="0" y="-8"/>
- <delta pt="20" x="4" y="-8"/>
- <delta pt="21" x="0" y="-8"/>
- <delta pt="22" x="0" y="-8"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="-2" y="0"/>
- <delta pt="1" x="-2" y="9"/>
- <delta pt="2" x="4" y="9"/>
- <delta pt="3" x="4" y="9"/>
- <delta pt="4" x="2" y="9"/>
- <delta pt="5" x="2" y="0"/>
- <delta pt="6" x="-2" y="0"/>
- <delta pt="7" x="4" y="0"/>
- <delta pt="8" x="4" y="0"/>
- <delta pt="9" x="4" y="0"/>
- <delta pt="10" x="4" y="0"/>
- <delta pt="11" x="6" y="-8"/>
- <delta pt="12" x="10" y="0"/>
- <delta pt="13" x="2" y="0"/>
- <delta pt="14" x="0" y="-5"/>
- <delta pt="15" x="3" y="5"/>
- <delta pt="16" x="-5" y="5"/>
- <delta pt="17" x="1" y="-5"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="0" y="8"/>
- <delta pt="20" x="-4" y="8"/>
- <delta pt="21" x="0" y="8"/>
- <delta pt="22" x="0" y="8"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="3" y="0"/>
- <delta pt="1" x="3" y="-15"/>
- <delta pt="2" x="-6" y="-15"/>
- <delta pt="3" x="-6" y="-15"/>
- <delta pt="4" x="-3" y="-15"/>
- <delta pt="5" x="-3" y="0"/>
- <delta pt="6" x="3" y="0"/>
- <delta pt="7" x="-6" y="0"/>
- <delta pt="8" x="-6" y="0"/>
- <delta pt="9" x="-6" y="0"/>
- <delta pt="10" x="-6" y="0"/>
- <delta pt="11" x="-11" y="13"/>
- <delta pt="12" x="-17" y="0"/>
- <delta pt="13" x="-3" y="0"/>
- <delta pt="14" x="-1" y="8"/>
- <delta pt="15" x="-5" y="-9"/>
- <delta pt="16" x="8" y="-9"/>
- <delta pt="17" x="-1" y="8"/>
- <delta pt="18" x="0" y="0"/>
- <delta pt="19" x="0" y="-13"/>
- <delta pt="20" x="6" y="-13"/>
- <delta pt="21" x="0" y="-13"/>
- <delta pt="22" x="0" y="-13"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="0"/>
- </tuple>
- </glyphVariations>
- <glyphVariations glyph="uni0061">
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <delta pt="0" x="11" y="-8"/>
- <delta pt="1" x="11" y="4"/>
- <delta pt="2" x="22" y="5"/>
- <delta pt="3" x="-4" y="-8"/>
- <delta pt="4" x="6" y="-5"/>
- <delta pt="5" x="3" y="-11"/>
- <delta pt="6" x="4" y="-9"/>
- <delta pt="7" x="4" y="9"/>
- <delta pt="8" x="0" y="7"/>
- <delta pt="9" x="-9" y="8"/>
- <delta pt="10" x="-24" y="3"/>
- <delta pt="11" x="-18" y="6"/>
- <delta pt="12" x="-44" y="1"/>
- <delta pt="13" x="-44" y="-16"/>
- <delta pt="14" x="-44" y="-22"/>
- <delta pt="15" x="-36" y="-39"/>
- <delta pt="16" x="-24" y="-39"/>
- <delta pt="17" x="-7" y="-39"/>
- <delta pt="18" x="26" y="-15"/>
- <delta pt="19" x="26" y="3"/>
- <delta pt="20" x="17" y="0"/>
- <delta pt="21" x="3" y="-4"/>
- <delta pt="22" x="23" y="15"/>
- <delta pt="23" x="22" y="8"/>
- <delta pt="24" x="6" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="2" y="0"/>
- <delta pt="27" x="11" y="-2"/>
- <delta pt="28" x="30" y="7"/>
- <delta pt="29" x="30" y="4"/>
- <delta pt="30" x="30" y="13"/>
- <delta pt="31" x="14" y="21"/>
- <delta pt="32" x="3" y="21"/>
- <delta pt="33" x="-15" y="21"/>
- <delta pt="34" x="-32" y="5"/>
- <delta pt="35" x="-34" y="-9"/>
- <delta pt="36" x="-48" y="-14"/>
- <delta pt="37" x="-40" y="4"/>
- <delta pt="38" x="-36" y="14"/>
- <delta pt="39" x="-24" y="27"/>
- <delta pt="40" x="-13" y="27"/>
- <delta pt="41" x="12" y="27"/>
- <delta pt="42" x="10" y="6"/>
- <delta pt="43" x="12" y="5"/>
- <delta pt="44" x="-4" y="-4"/>
- <delta pt="45" x="-16" y="-4"/>
- <delta pt="46" x="-20" y="-4"/>
- <delta pt="47" x="-22" y="7"/>
- <delta pt="48" x="-22" y="25"/>
- <delta pt="49" x="-22" y="10"/>
- <delta pt="50" x="-22" y="-15"/>
- <delta pt="51" x="-16" y="-30"/>
- <delta pt="52" x="-9" y="-30"/>
- <delta pt="53" x="-12" y="-30"/>
- <delta pt="54" x="-11" y="-35"/>
- <delta pt="55" x="-5" y="-35"/>
- <delta pt="56" x="-15" y="-27"/>
- <delta pt="57" x="-10" y="-3"/>
- <delta pt="58" x="9" y="-3"/>
- <delta pt="59" x="14" y="-3"/>
- <delta pt="60" x="33" y="-1"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="-3" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-21" y="1"/>
- <delta pt="1" x="-21" y="17"/>
- <delta pt="2" x="-2" y="28"/>
- <delta pt="3" x="20" y="23"/>
- <delta pt="4" x="19" y="20"/>
- <delta pt="5" x="28" y="21"/>
- <delta pt="6" x="26" y="23"/>
- <delta pt="7" x="26" y="15"/>
- <delta pt="8" x="24" y="12"/>
- <delta pt="9" x="30" y="17"/>
- <delta pt="10" x="31" y="15"/>
- <delta pt="11" x="77" y="31"/>
- <delta pt="12" x="66" y="36"/>
- <delta pt="13" x="66" y="18"/>
- <delta pt="14" x="66" y="21"/>
- <delta pt="15" x="49" y="19"/>
- <delta pt="16" x="37" y="19"/>
- <delta pt="17" x="21" y="19"/>
- <delta pt="18" x="-2" y="5"/>
- <delta pt="19" x="-34" y="-18"/>
- <delta pt="20" x="-6" y="3"/>
- <delta pt="21" x="-11" y="12"/>
- <delta pt="22" x="-29" y="-11"/>
- <delta pt="23" x="-17" y="-2"/>
- <delta pt="24" x="-13" y="-3"/>
- <delta pt="25" x="-25" y="-3"/>
- <delta pt="26" x="-29" y="-3"/>
- <delta pt="27" x="-21" y="2"/>
- <delta pt="28" x="-34" y="-14"/>
- <delta pt="29" x="-34" y="17"/>
- <delta pt="30" x="-34" y="7"/>
- <delta pt="31" x="-18" y="7"/>
- <delta pt="32" x="-16" y="7"/>
- <delta pt="33" x="-18" y="7"/>
- <delta pt="34" x="-15" y="9"/>
- <delta pt="35" x="-21" y="12"/>
- <delta pt="36" x="19" y="23"/>
- <delta pt="37" x="45" y="46"/>
- <delta pt="38" x="52" y="7"/>
- <delta pt="39" x="26" y="-21"/>
- <delta pt="40" x="14" y="-21"/>
- <delta pt="41" x="-5" y="-21"/>
- <delta pt="42" x="-17" y="-7"/>
- <delta pt="43" x="-31" y="1"/>
- <delta pt="44" x="-12" y="16"/>
- <delta pt="45" x="34" y="16"/>
- <delta pt="46" x="61" y="16"/>
- <delta pt="47" x="70" y="4"/>
- <delta pt="48" x="70" y="-5"/>
- <delta pt="49" x="70" y="-22"/>
- <delta pt="50" x="70" y="4"/>
- <delta pt="51" x="59" y="22"/>
- <delta pt="52" x="50" y="22"/>
- <delta pt="53" x="43" y="22"/>
- <delta pt="54" x="37" y="19"/>
- <delta pt="55" x="38" y="22"/>
- <delta pt="56" x="47" y="28"/>
- <delta pt="57" x="46" y="-6"/>
- <delta pt="58" x="-2" y="-6"/>
- <delta pt="59" x="-16" y="-6"/>
- <delta pt="60" x="-25" y="-13"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="32" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="0" y="-3"/>
- <delta pt="1" x="0" y="-1"/>
- <delta pt="2" x="0" y="-3"/>
- <delta pt="3" x="0" y="-3"/>
- <delta pt="4" x="0" y="-3"/>
- <delta pt="5" x="0" y="-3"/>
- <delta pt="6" x="0" y="-3"/>
- <delta pt="7" x="0" y="4"/>
- <delta pt="8" x="0" y="4"/>
- <delta pt="9" x="2" y="5"/>
- <delta pt="10" x="6" y="7"/>
- <delta pt="11" x="1" y="5"/>
- <delta pt="12" x="0" y="-1"/>
- <delta pt="13" x="0" y="-6"/>
- <delta pt="14" x="0" y="-6"/>
- <delta pt="15" x="-1" y="-6"/>
- <delta pt="16" x="0" y="-6"/>
- <delta pt="17" x="0" y="-6"/>
- <delta pt="18" x="0" y="-5"/>
- <delta pt="19" x="0" y="-4"/>
- <delta pt="20" x="0" y="-1"/>
- <delta pt="21" x="0" y="0"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="-1"/>
- <delta pt="28" x="0" y="-2"/>
- <delta pt="29" x="0" y="7"/>
- <delta pt="30" x="0" y="6"/>
- <delta pt="31" x="0" y="7"/>
- <delta pt="32" x="0" y="7"/>
- <delta pt="33" x="0" y="7"/>
- <delta pt="34" x="0" y="7"/>
- <delta pt="35" x="0" y="7"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="0" y="0"/>
- <delta pt="38" x="0" y="0"/>
- <delta pt="39" x="0" y="0"/>
- <delta pt="40" x="0" y="0"/>
- <delta pt="41" x="0" y="0"/>
- <delta pt="42" x="0" y="0"/>
- <delta pt="43" x="0" y="0"/>
- <delta pt="44" x="0" y="0"/>
- <delta pt="45" x="0" y="0"/>
- <delta pt="46" x="0" y="0"/>
- <delta pt="47" x="0" y="0"/>
- <delta pt="48" x="0" y="0"/>
- <delta pt="49" x="0" y="-6"/>
- <delta pt="50" x="0" y="-7"/>
- <delta pt="51" x="0" y="-8"/>
- <delta pt="52" x="0" y="-8"/>
- <delta pt="53" x="1" y="-8"/>
- <delta pt="54" x="2" y="-5"/>
- <delta pt="55" x="4" y="-2"/>
- <delta pt="56" x="0" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="0" y="0"/>
- <delta pt="59" x="0" y="0"/>
- <delta pt="60" x="0" y="-1"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="0" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="-1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="0" y="3"/>
- <delta pt="1" x="0" y="1"/>
- <delta pt="2" x="0" y="3"/>
- <delta pt="3" x="0" y="3"/>
- <delta pt="4" x="0" y="3"/>
- <delta pt="5" x="0" y="3"/>
- <delta pt="6" x="0" y="3"/>
- <delta pt="7" x="0" y="-4"/>
- <delta pt="8" x="0" y="-4"/>
- <delta pt="9" x="-2" y="-5"/>
- <delta pt="10" x="-6" y="-7"/>
- <delta pt="11" x="-1" y="-5"/>
- <delta pt="12" x="0" y="1"/>
- <delta pt="13" x="0" y="6"/>
- <delta pt="14" x="0" y="6"/>
- <delta pt="15" x="1" y="6"/>
- <delta pt="16" x="0" y="6"/>
- <delta pt="17" x="0" y="6"/>
- <delta pt="18" x="0" y="5"/>
- <delta pt="19" x="0" y="4"/>
- <delta pt="20" x="0" y="1"/>
- <delta pt="21" x="0" y="0"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="0" y="0"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="1"/>
- <delta pt="28" x="0" y="2"/>
- <delta pt="29" x="0" y="-7"/>
- <delta pt="30" x="0" y="-6"/>
- <delta pt="31" x="0" y="-7"/>
- <delta pt="32" x="0" y="-7"/>
- <delta pt="33" x="0" y="-7"/>
- <delta pt="34" x="0" y="-7"/>
- <delta pt="35" x="0" y="-7"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="0" y="0"/>
- <delta pt="38" x="0" y="0"/>
- <delta pt="39" x="0" y="0"/>
- <delta pt="40" x="0" y="0"/>
- <delta pt="41" x="0" y="0"/>
- <delta pt="42" x="0" y="0"/>
- <delta pt="43" x="0" y="0"/>
- <delta pt="44" x="0" y="0"/>
- <delta pt="45" x="0" y="0"/>
- <delta pt="46" x="0" y="0"/>
- <delta pt="47" x="0" y="0"/>
- <delta pt="48" x="0" y="0"/>
- <delta pt="49" x="0" y="6"/>
- <delta pt="50" x="0" y="7"/>
- <delta pt="51" x="0" y="8"/>
- <delta pt="52" x="0" y="8"/>
- <delta pt="53" x="-1" y="8"/>
- <delta pt="54" x="-2" y="5"/>
- <delta pt="55" x="-4" y="2"/>
- <delta pt="56" x="0" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="0" y="0"/>
- <delta pt="59" x="0" y="0"/>
- <delta pt="60" x="0" y="1"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="0" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- <tuple>
- <coord axis="wght" value="1.0"/>
- <coord axis="cntr" value="1.0"/>
- <delta pt="0" x="0" y="-5"/>
- <delta pt="1" x="0" y="0"/>
- <delta pt="2" x="3" y="-4"/>
- <delta pt="3" x="0" y="-4"/>
- <delta pt="4" x="0" y="-4"/>
- <delta pt="5" x="0" y="-4"/>
- <delta pt="6" x="0" y="-4"/>
- <delta pt="7" x="0" y="8"/>
- <delta pt="8" x="0" y="8"/>
- <delta pt="9" x="5" y="9"/>
- <delta pt="10" x="11" y="13"/>
- <delta pt="11" x="2" y="10"/>
- <delta pt="12" x="0" y="0"/>
- <delta pt="13" x="0" y="-9"/>
- <delta pt="14" x="0" y="-9"/>
- <delta pt="15" x="-1" y="-9"/>
- <delta pt="16" x="0" y="-9"/>
- <delta pt="17" x="0" y="-9"/>
- <delta pt="18" x="0" y="-10"/>
- <delta pt="19" x="0" y="-8"/>
- <delta pt="20" x="0" y="-2"/>
- <delta pt="21" x="0" y="1"/>
- <delta pt="22" x="0" y="0"/>
- <delta pt="23" x="1" y="-1"/>
- <delta pt="24" x="0" y="0"/>
- <delta pt="25" x="0" y="0"/>
- <delta pt="26" x="0" y="0"/>
- <delta pt="27" x="0" y="-1"/>
- <delta pt="28" x="0" y="-4"/>
- <delta pt="29" x="0" y="12"/>
- <delta pt="30" x="0" y="13"/>
- <delta pt="31" x="0" y="13"/>
- <delta pt="32" x="0" y="13"/>
- <delta pt="33" x="0" y="13"/>
- <delta pt="34" x="0" y="13"/>
- <delta pt="35" x="0" y="13"/>
- <delta pt="36" x="0" y="0"/>
- <delta pt="37" x="0" y="0"/>
- <delta pt="38" x="0" y="0"/>
- <delta pt="39" x="0" y="1"/>
- <delta pt="40" x="0" y="1"/>
- <delta pt="41" x="0" y="1"/>
- <delta pt="42" x="0" y="1"/>
- <delta pt="43" x="0" y="0"/>
- <delta pt="44" x="0" y="0"/>
- <delta pt="45" x="0" y="0"/>
- <delta pt="46" x="0" y="0"/>
- <delta pt="47" x="0" y="-1"/>
- <delta pt="48" x="0" y="-1"/>
- <delta pt="49" x="0" y="-9"/>
- <delta pt="50" x="0" y="-13"/>
- <delta pt="51" x="1" y="-14"/>
- <delta pt="52" x="1" y="-14"/>
- <delta pt="53" x="2" y="-14"/>
- <delta pt="54" x="5" y="-11"/>
- <delta pt="55" x="7" y="-4"/>
- <delta pt="56" x="0" y="0"/>
- <delta pt="57" x="0" y="0"/>
- <delta pt="58" x="0" y="0"/>
- <delta pt="59" x="0" y="0"/>
- <delta pt="60" x="1" y="0"/>
- <delta pt="61" x="0" y="0"/>
- <delta pt="62" x="0" y="0"/>
- <delta pt="63" x="0" y="0"/>
- <delta pt="64" x="0" y="0"/>
- </tuple>
- </glyphVariations>
<glyphVariations glyph="uni0024">
<tuple>
<coord axis="wght" value="-1.0"/>
@@ -2252,6 +1749,509 @@
<delta pt="65" x="0" y="0"/>
</tuple>
</glyphVariations>
+ <glyphVariations glyph="uni0041">
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <delta pt="0" x="7" y="0"/>
+ <delta pt="1" x="7" y="-20"/>
+ <delta pt="2" x="-6" y="-29"/>
+ <delta pt="3" x="-12" y="-29"/>
+ <delta pt="4" x="-25" y="-20"/>
+ <delta pt="5" x="-25" y="0"/>
+ <delta pt="6" x="14" y="0"/>
+ <delta pt="7" x="4" y="9"/>
+ <delta pt="8" x="-36" y="9"/>
+ <delta pt="9" x="-37" y="0"/>
+ <delta pt="10" x="24" y="0"/>
+ <delta pt="11" x="9" y="58"/>
+ <delta pt="12" x="3" y="68"/>
+ <delta pt="13" x="-4" y="0"/>
+ <delta pt="14" x="3" y="28"/>
+ <delta pt="15" x="-4" y="2"/>
+ <delta pt="16" x="4" y="2"/>
+ <delta pt="17" x="-4" y="28"/>
+ <delta pt="18" x="20" y="0"/>
+ <delta pt="19" x="20" y="-20"/>
+ <delta pt="20" x="14" y="-29"/>
+ <delta pt="21" x="8" y="-29"/>
+ <delta pt="22" x="-2" y="-20"/>
+ <delta pt="23" x="-2" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="-10" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="5" y="0"/>
+ <delta pt="1" x="5" y="19"/>
+ <delta pt="2" x="9" y="19"/>
+ <delta pt="3" x="6" y="19"/>
+ <delta pt="4" x="-15" y="19"/>
+ <delta pt="5" x="-15" y="0"/>
+ <delta pt="6" x="-6" y="0"/>
+ <delta pt="7" x="-14" y="-23"/>
+ <delta pt="8" x="46" y="-23"/>
+ <delta pt="9" x="39" y="0"/>
+ <delta pt="10" x="-69" y="0"/>
+ <delta pt="11" x="-27" y="-86"/>
+ <delta pt="12" x="-7" y="-16"/>
+ <delta pt="13" x="11" y="0"/>
+ <delta pt="14" x="-2" y="-39"/>
+ <delta pt="15" x="-1" y="-22"/>
+ <delta pt="16" x="-1" y="-22"/>
+ <delta pt="17" x="8" y="-39"/>
+ <delta pt="18" x="-41" y="0"/>
+ <delta pt="19" x="-41" y="16"/>
+ <delta pt="20" x="-59" y="16"/>
+ <delta pt="21" x="6" y="16"/>
+ <delta pt="22" x="12" y="16"/>
+ <delta pt="23" x="12" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="17" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="2" y="0"/>
+ <delta pt="1" x="2" y="-9"/>
+ <delta pt="2" x="-4" y="-9"/>
+ <delta pt="3" x="-4" y="-9"/>
+ <delta pt="4" x="-2" y="-9"/>
+ <delta pt="5" x="-2" y="0"/>
+ <delta pt="6" x="2" y="0"/>
+ <delta pt="7" x="-4" y="0"/>
+ <delta pt="8" x="-4" y="0"/>
+ <delta pt="9" x="-4" y="0"/>
+ <delta pt="10" x="-4" y="0"/>
+ <delta pt="11" x="-6" y="8"/>
+ <delta pt="12" x="-10" y="0"/>
+ <delta pt="13" x="-2" y="0"/>
+ <delta pt="14" x="0" y="5"/>
+ <delta pt="15" x="-3" y="-5"/>
+ <delta pt="16" x="5" y="-5"/>
+ <delta pt="17" x="-1" y="5"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="-8"/>
+ <delta pt="20" x="4" y="-8"/>
+ <delta pt="21" x="0" y="-8"/>
+ <delta pt="22" x="0" y="-8"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="-2" y="0"/>
+ <delta pt="1" x="-2" y="9"/>
+ <delta pt="2" x="4" y="9"/>
+ <delta pt="3" x="4" y="9"/>
+ <delta pt="4" x="2" y="9"/>
+ <delta pt="5" x="2" y="0"/>
+ <delta pt="6" x="-2" y="0"/>
+ <delta pt="7" x="4" y="0"/>
+ <delta pt="8" x="4" y="0"/>
+ <delta pt="9" x="4" y="0"/>
+ <delta pt="10" x="4" y="0"/>
+ <delta pt="11" x="6" y="-8"/>
+ <delta pt="12" x="10" y="0"/>
+ <delta pt="13" x="2" y="0"/>
+ <delta pt="14" x="0" y="-5"/>
+ <delta pt="15" x="3" y="5"/>
+ <delta pt="16" x="-5" y="5"/>
+ <delta pt="17" x="1" y="-5"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="8"/>
+ <delta pt="20" x="-4" y="8"/>
+ <delta pt="21" x="0" y="8"/>
+ <delta pt="22" x="0" y="8"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="3" y="0"/>
+ <delta pt="1" x="3" y="-15"/>
+ <delta pt="2" x="-6" y="-15"/>
+ <delta pt="3" x="-6" y="-15"/>
+ <delta pt="4" x="-3" y="-15"/>
+ <delta pt="5" x="-3" y="0"/>
+ <delta pt="6" x="3" y="0"/>
+ <delta pt="7" x="-6" y="0"/>
+ <delta pt="8" x="-6" y="0"/>
+ <delta pt="9" x="-6" y="0"/>
+ <delta pt="10" x="-6" y="0"/>
+ <delta pt="11" x="-11" y="13"/>
+ <delta pt="12" x="-17" y="0"/>
+ <delta pt="13" x="-3" y="0"/>
+ <delta pt="14" x="-1" y="8"/>
+ <delta pt="15" x="-5" y="-9"/>
+ <delta pt="16" x="8" y="-9"/>
+ <delta pt="17" x="-1" y="8"/>
+ <delta pt="18" x="0" y="0"/>
+ <delta pt="19" x="0" y="-13"/>
+ <delta pt="20" x="6" y="-13"/>
+ <delta pt="21" x="0" y="-13"/>
+ <delta pt="22" x="0" y="-13"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
+ <glyphVariations glyph="uni0061">
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <delta pt="0" x="11" y="-8"/>
+ <delta pt="1" x="11" y="4"/>
+ <delta pt="2" x="22" y="5"/>
+ <delta pt="3" x="-4" y="-8"/>
+ <delta pt="4" x="6" y="-5"/>
+ <delta pt="5" x="3" y="-11"/>
+ <delta pt="6" x="4" y="-9"/>
+ <delta pt="7" x="4" y="9"/>
+ <delta pt="8" x="0" y="7"/>
+ <delta pt="9" x="-9" y="8"/>
+ <delta pt="10" x="-24" y="3"/>
+ <delta pt="11" x="-18" y="6"/>
+ <delta pt="12" x="-44" y="1"/>
+ <delta pt="13" x="-44" y="-16"/>
+ <delta pt="14" x="-44" y="-22"/>
+ <delta pt="15" x="-36" y="-39"/>
+ <delta pt="16" x="-24" y="-39"/>
+ <delta pt="17" x="-7" y="-39"/>
+ <delta pt="18" x="26" y="-15"/>
+ <delta pt="19" x="26" y="3"/>
+ <delta pt="20" x="17" y="0"/>
+ <delta pt="21" x="3" y="-4"/>
+ <delta pt="22" x="23" y="15"/>
+ <delta pt="23" x="22" y="8"/>
+ <delta pt="24" x="6" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="2" y="0"/>
+ <delta pt="27" x="11" y="-2"/>
+ <delta pt="28" x="30" y="7"/>
+ <delta pt="29" x="30" y="4"/>
+ <delta pt="30" x="30" y="13"/>
+ <delta pt="31" x="14" y="21"/>
+ <delta pt="32" x="3" y="21"/>
+ <delta pt="33" x="-15" y="21"/>
+ <delta pt="34" x="-32" y="5"/>
+ <delta pt="35" x="-34" y="-9"/>
+ <delta pt="36" x="-48" y="-14"/>
+ <delta pt="37" x="-40" y="4"/>
+ <delta pt="38" x="-36" y="14"/>
+ <delta pt="39" x="-24" y="27"/>
+ <delta pt="40" x="-13" y="27"/>
+ <delta pt="41" x="12" y="27"/>
+ <delta pt="42" x="10" y="6"/>
+ <delta pt="43" x="12" y="5"/>
+ <delta pt="44" x="-4" y="-4"/>
+ <delta pt="45" x="-16" y="-4"/>
+ <delta pt="46" x="-20" y="-4"/>
+ <delta pt="47" x="-22" y="7"/>
+ <delta pt="48" x="-22" y="25"/>
+ <delta pt="49" x="-22" y="10"/>
+ <delta pt="50" x="-22" y="-15"/>
+ <delta pt="51" x="-16" y="-30"/>
+ <delta pt="52" x="-9" y="-30"/>
+ <delta pt="53" x="-12" y="-30"/>
+ <delta pt="54" x="-11" y="-35"/>
+ <delta pt="55" x="-5" y="-35"/>
+ <delta pt="56" x="-15" y="-27"/>
+ <delta pt="57" x="-10" y="-3"/>
+ <delta pt="58" x="9" y="-3"/>
+ <delta pt="59" x="14" y="-3"/>
+ <delta pt="60" x="33" y="-1"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="-3" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="-21" y="1"/>
+ <delta pt="1" x="-21" y="17"/>
+ <delta pt="2" x="-2" y="28"/>
+ <delta pt="3" x="20" y="23"/>
+ <delta pt="4" x="19" y="20"/>
+ <delta pt="5" x="28" y="21"/>
+ <delta pt="6" x="26" y="23"/>
+ <delta pt="7" x="26" y="15"/>
+ <delta pt="8" x="24" y="12"/>
+ <delta pt="9" x="30" y="17"/>
+ <delta pt="10" x="31" y="15"/>
+ <delta pt="11" x="77" y="31"/>
+ <delta pt="12" x="66" y="36"/>
+ <delta pt="13" x="66" y="18"/>
+ <delta pt="14" x="66" y="21"/>
+ <delta pt="15" x="49" y="19"/>
+ <delta pt="16" x="37" y="19"/>
+ <delta pt="17" x="21" y="19"/>
+ <delta pt="18" x="-2" y="5"/>
+ <delta pt="19" x="-34" y="-18"/>
+ <delta pt="20" x="-6" y="3"/>
+ <delta pt="21" x="-11" y="12"/>
+ <delta pt="22" x="-29" y="-11"/>
+ <delta pt="23" x="-17" y="-2"/>
+ <delta pt="24" x="-13" y="-3"/>
+ <delta pt="25" x="-25" y="-3"/>
+ <delta pt="26" x="-29" y="-3"/>
+ <delta pt="27" x="-21" y="2"/>
+ <delta pt="28" x="-34" y="-14"/>
+ <delta pt="29" x="-34" y="17"/>
+ <delta pt="30" x="-34" y="7"/>
+ <delta pt="31" x="-18" y="7"/>
+ <delta pt="32" x="-16" y="7"/>
+ <delta pt="33" x="-18" y="7"/>
+ <delta pt="34" x="-15" y="9"/>
+ <delta pt="35" x="-21" y="12"/>
+ <delta pt="36" x="19" y="23"/>
+ <delta pt="37" x="45" y="46"/>
+ <delta pt="38" x="52" y="7"/>
+ <delta pt="39" x="26" y="-21"/>
+ <delta pt="40" x="14" y="-21"/>
+ <delta pt="41" x="-5" y="-21"/>
+ <delta pt="42" x="-17" y="-7"/>
+ <delta pt="43" x="-31" y="1"/>
+ <delta pt="44" x="-12" y="16"/>
+ <delta pt="45" x="34" y="16"/>
+ <delta pt="46" x="61" y="16"/>
+ <delta pt="47" x="70" y="4"/>
+ <delta pt="48" x="70" y="-5"/>
+ <delta pt="49" x="70" y="-22"/>
+ <delta pt="50" x="70" y="4"/>
+ <delta pt="51" x="59" y="22"/>
+ <delta pt="52" x="50" y="22"/>
+ <delta pt="53" x="43" y="22"/>
+ <delta pt="54" x="37" y="19"/>
+ <delta pt="55" x="38" y="22"/>
+ <delta pt="56" x="47" y="28"/>
+ <delta pt="57" x="46" y="-6"/>
+ <delta pt="58" x="-2" y="-6"/>
+ <delta pt="59" x="-16" y="-6"/>
+ <delta pt="60" x="-25" y="-13"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="32" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="0" y="-3"/>
+ <delta pt="1" x="0" y="-1"/>
+ <delta pt="2" x="0" y="-3"/>
+ <delta pt="3" x="0" y="-3"/>
+ <delta pt="4" x="0" y="-3"/>
+ <delta pt="5" x="0" y="-3"/>
+ <delta pt="6" x="0" y="-3"/>
+ <delta pt="7" x="0" y="4"/>
+ <delta pt="8" x="0" y="4"/>
+ <delta pt="9" x="2" y="5"/>
+ <delta pt="10" x="6" y="7"/>
+ <delta pt="11" x="1" y="5"/>
+ <delta pt="12" x="0" y="-1"/>
+ <delta pt="13" x="0" y="-6"/>
+ <delta pt="14" x="0" y="-6"/>
+ <delta pt="15" x="-1" y="-6"/>
+ <delta pt="16" x="0" y="-6"/>
+ <delta pt="17" x="0" y="-6"/>
+ <delta pt="18" x="0" y="-5"/>
+ <delta pt="19" x="0" y="-4"/>
+ <delta pt="20" x="0" y="-1"/>
+ <delta pt="21" x="0" y="0"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="-1"/>
+ <delta pt="28" x="0" y="-2"/>
+ <delta pt="29" x="0" y="7"/>
+ <delta pt="30" x="0" y="6"/>
+ <delta pt="31" x="0" y="7"/>
+ <delta pt="32" x="0" y="7"/>
+ <delta pt="33" x="0" y="7"/>
+ <delta pt="34" x="0" y="7"/>
+ <delta pt="35" x="0" y="7"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="0"/>
+ <delta pt="40" x="0" y="0"/>
+ <delta pt="41" x="0" y="0"/>
+ <delta pt="42" x="0" y="0"/>
+ <delta pt="43" x="0" y="0"/>
+ <delta pt="44" x="0" y="0"/>
+ <delta pt="45" x="0" y="0"/>
+ <delta pt="46" x="0" y="0"/>
+ <delta pt="47" x="0" y="0"/>
+ <delta pt="48" x="0" y="0"/>
+ <delta pt="49" x="0" y="-6"/>
+ <delta pt="50" x="0" y="-7"/>
+ <delta pt="51" x="0" y="-8"/>
+ <delta pt="52" x="0" y="-8"/>
+ <delta pt="53" x="1" y="-8"/>
+ <delta pt="54" x="2" y="-5"/>
+ <delta pt="55" x="4" y="-2"/>
+ <delta pt="56" x="0" y="0"/>
+ <delta pt="57" x="0" y="0"/>
+ <delta pt="58" x="0" y="0"/>
+ <delta pt="59" x="0" y="0"/>
+ <delta pt="60" x="0" y="-1"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="0" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="-1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="0" y="3"/>
+ <delta pt="1" x="0" y="1"/>
+ <delta pt="2" x="0" y="3"/>
+ <delta pt="3" x="0" y="3"/>
+ <delta pt="4" x="0" y="3"/>
+ <delta pt="5" x="0" y="3"/>
+ <delta pt="6" x="0" y="3"/>
+ <delta pt="7" x="0" y="-4"/>
+ <delta pt="8" x="0" y="-4"/>
+ <delta pt="9" x="-2" y="-5"/>
+ <delta pt="10" x="-6" y="-7"/>
+ <delta pt="11" x="-1" y="-5"/>
+ <delta pt="12" x="0" y="1"/>
+ <delta pt="13" x="0" y="6"/>
+ <delta pt="14" x="0" y="6"/>
+ <delta pt="15" x="1" y="6"/>
+ <delta pt="16" x="0" y="6"/>
+ <delta pt="17" x="0" y="6"/>
+ <delta pt="18" x="0" y="5"/>
+ <delta pt="19" x="0" y="4"/>
+ <delta pt="20" x="0" y="1"/>
+ <delta pt="21" x="0" y="0"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="0" y="0"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="1"/>
+ <delta pt="28" x="0" y="2"/>
+ <delta pt="29" x="0" y="-7"/>
+ <delta pt="30" x="0" y="-6"/>
+ <delta pt="31" x="0" y="-7"/>
+ <delta pt="32" x="0" y="-7"/>
+ <delta pt="33" x="0" y="-7"/>
+ <delta pt="34" x="0" y="-7"/>
+ <delta pt="35" x="0" y="-7"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="0"/>
+ <delta pt="40" x="0" y="0"/>
+ <delta pt="41" x="0" y="0"/>
+ <delta pt="42" x="0" y="0"/>
+ <delta pt="43" x="0" y="0"/>
+ <delta pt="44" x="0" y="0"/>
+ <delta pt="45" x="0" y="0"/>
+ <delta pt="46" x="0" y="0"/>
+ <delta pt="47" x="0" y="0"/>
+ <delta pt="48" x="0" y="0"/>
+ <delta pt="49" x="0" y="6"/>
+ <delta pt="50" x="0" y="7"/>
+ <delta pt="51" x="0" y="8"/>
+ <delta pt="52" x="0" y="8"/>
+ <delta pt="53" x="-1" y="8"/>
+ <delta pt="54" x="-2" y="5"/>
+ <delta pt="55" x="-4" y="2"/>
+ <delta pt="56" x="0" y="0"/>
+ <delta pt="57" x="0" y="0"/>
+ <delta pt="58" x="0" y="0"/>
+ <delta pt="59" x="0" y="0"/>
+ <delta pt="60" x="0" y="1"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="0" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <coord axis="cntr" value="1.0"/>
+ <delta pt="0" x="0" y="-5"/>
+ <delta pt="1" x="0" y="0"/>
+ <delta pt="2" x="3" y="-4"/>
+ <delta pt="3" x="0" y="-4"/>
+ <delta pt="4" x="0" y="-4"/>
+ <delta pt="5" x="0" y="-4"/>
+ <delta pt="6" x="0" y="-4"/>
+ <delta pt="7" x="0" y="8"/>
+ <delta pt="8" x="0" y="8"/>
+ <delta pt="9" x="5" y="9"/>
+ <delta pt="10" x="11" y="13"/>
+ <delta pt="11" x="2" y="10"/>
+ <delta pt="12" x="0" y="0"/>
+ <delta pt="13" x="0" y="-9"/>
+ <delta pt="14" x="0" y="-9"/>
+ <delta pt="15" x="-1" y="-9"/>
+ <delta pt="16" x="0" y="-9"/>
+ <delta pt="17" x="0" y="-9"/>
+ <delta pt="18" x="0" y="-10"/>
+ <delta pt="19" x="0" y="-8"/>
+ <delta pt="20" x="0" y="-2"/>
+ <delta pt="21" x="0" y="1"/>
+ <delta pt="22" x="0" y="0"/>
+ <delta pt="23" x="1" y="-1"/>
+ <delta pt="24" x="0" y="0"/>
+ <delta pt="25" x="0" y="0"/>
+ <delta pt="26" x="0" y="0"/>
+ <delta pt="27" x="0" y="-1"/>
+ <delta pt="28" x="0" y="-4"/>
+ <delta pt="29" x="0" y="12"/>
+ <delta pt="30" x="0" y="13"/>
+ <delta pt="31" x="0" y="13"/>
+ <delta pt="32" x="0" y="13"/>
+ <delta pt="33" x="0" y="13"/>
+ <delta pt="34" x="0" y="13"/>
+ <delta pt="35" x="0" y="13"/>
+ <delta pt="36" x="0" y="0"/>
+ <delta pt="37" x="0" y="0"/>
+ <delta pt="38" x="0" y="0"/>
+ <delta pt="39" x="0" y="1"/>
+ <delta pt="40" x="0" y="1"/>
+ <delta pt="41" x="0" y="1"/>
+ <delta pt="42" x="0" y="1"/>
+ <delta pt="43" x="0" y="0"/>
+ <delta pt="44" x="0" y="0"/>
+ <delta pt="45" x="0" y="0"/>
+ <delta pt="46" x="0" y="0"/>
+ <delta pt="47" x="0" y="-1"/>
+ <delta pt="48" x="0" y="-1"/>
+ <delta pt="49" x="0" y="-9"/>
+ <delta pt="50" x="0" y="-13"/>
+ <delta pt="51" x="1" y="-14"/>
+ <delta pt="52" x="1" y="-14"/>
+ <delta pt="53" x="2" y="-14"/>
+ <delta pt="54" x="5" y="-11"/>
+ <delta pt="55" x="7" y="-4"/>
+ <delta pt="56" x="0" y="0"/>
+ <delta pt="57" x="0" y="0"/>
+ <delta pt="58" x="0" y="0"/>
+ <delta pt="59" x="0" y="0"/>
+ <delta pt="60" x="1" y="0"/>
+ <delta pt="61" x="0" y="0"/>
+ <delta pt="62" x="0" y="0"/>
+ <delta pt="63" x="0" y="0"/>
+ <delta pt="64" x="0" y="0"/>
+ </tuple>
+ </glyphVariations>
</gvar>
<ltag>
diff --git a/Tests/varLib/data/test_results/FeatureVarsWholeRange.ttx b/Tests/varLib/data/test_results/FeatureVarsWholeRange.ttx
new file mode 100644
index 00000000..8e098b9b
--- /dev/null
+++ b/Tests/varLib/data/test_results/FeatureVarsWholeRange.ttx
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9">
+
+ <fvar>
+
+ <!-- Weight -->
+ <Axis>
+ <AxisTag>wght</AxisTag>
+ <Flags>0x0</Flags>
+ <MinValue>0.0</MinValue>
+ <DefaultValue>368.0</DefaultValue>
+ <MaxValue>1000.0</MaxValue>
+ <AxisNameID>256</AxisNameID>
+ </Axis>
+ </fvar>
+
+ <GSUB>
+ <Version value="0x00010001"/>
+ <ScriptList>
+ <!-- ScriptCount=1 -->
+ <ScriptRecord index="0">
+ <ScriptTag value="DFLT"/>
+ <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="rclt"/>
+ <Feature>
+ <!-- LookupCount=0 -->
+ </Feature>
+ </FeatureRecord>
+ </FeatureList>
+ <LookupList>
+ <!-- LookupCount=1 -->
+ <Lookup index="0">
+ <LookupType value="1"/>
+ <LookupFlag value="0"/>
+ <!-- SubTableCount=1 -->
+ <SingleSubst index="0" Format="1">
+ <Substitution in="uni0024" out="uni0024.nostroke"/>
+ </SingleSubst>
+ </Lookup>
+ </LookupList>
+ <FeatureVariations>
+ <Version value="0x00010000"/>
+ <!-- FeatureVariationCount=1 -->
+ <FeatureVariationRecord index="0">
+ <ConditionSet>
+ <!-- ConditionCount=0 -->
+ </ConditionSet>
+ <FeatureTableSubstitution>
+ <Version value="0x00010000"/>
+ <!-- SubstitutionCount=1 -->
+ <SubstitutionRecord index="0">
+ <FeatureIndex value="0"/>
+ <Feature>
+ <!-- LookupCount=1 -->
+ <LookupListIndex index="0" value="0"/>
+ </Feature>
+ </SubstitutionRecord>
+ </FeatureTableSubstitution>
+ </FeatureVariationRecord>
+ </FeatureVariations>
+ </GSUB>
+
+</ttFont>
diff --git a/Tests/varLib/data/test_results/SparseMasters.ttx b/Tests/varLib/data/test_results/SparseMasters.ttx
index c2aa335c..fb9cb46d 100644
--- a/Tests/varLib/data/test_results/SparseMasters.ttx
+++ b/Tests/varLib/data/test_results/SparseMasters.ttx
@@ -572,6 +572,19 @@
<delta pt="21" x="0" y="0"/>
</tuple>
</glyphVariations>
+ <glyphVariations glyph="dotabovecomb">
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="0" x="-8" y="28"/>
+ <delta pt="1" x="13" y="16"/>
+ <delta pt="2" x="17" y="-13"/>
+ <delta pt="3" x="-27" y="-20"/>
+ <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>
<glyphVariations glyph="e">
<tuple>
<coord axis="wght" min="0.0" value="0.36365" max="1.0"/>
@@ -614,6 +627,12 @@
<delta pt="16" x="0" y="0"/>
</tuple>
</glyphVariations>
+ <glyphVariations glyph="edotabove">
+ <tuple>
+ <coord axis="wght" value="1.0"/>
+ <delta pt="1" x="-6" y="91"/>
+ </tuple>
+ </glyphVariations>
<glyphVariations glyph="s">
<tuple>
<coord axis="wght" value="1.0"/>
@@ -635,25 +654,6 @@
<delta pt="15" x="0" y="0"/>
</tuple>
</glyphVariations>
- <glyphVariations glyph="dotabovecomb">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="0" x="-8" y="28"/>
- <delta pt="1" x="13" y="16"/>
- <delta pt="2" x="17" y="-13"/>
- <delta pt="3" x="-27" y="-20"/>
- <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>
- <glyphVariations glyph="edotabove">
- <tuple>
- <coord axis="wght" value="1.0"/>
- <delta pt="1" x="-6" y="91"/>
- </tuple>
- </glyphVariations>
</gvar>
</ttFont>
diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py
index 2c95784a..5d36d687 100644
--- a/Tests/varLib/varLib_test.py
+++ b/Tests/varLib/varLib_test.py
@@ -218,6 +218,30 @@ class BuildTest(unittest.TestCase):
save_before_dump=True,
)
+ def test_varlib_build_feature_variations_whole_range(self):
+ """Designspace file contains <rules> element specifying the entire design
+ space, used to build GSUB FeatureVariations table.
+ """
+ self._run_varlib_build_test(
+ designspace_name="FeatureVarsWholeRange",
+ font_name="TestFamily",
+ tables=["fvar", "GSUB"],
+ expected_ttx_name="FeatureVarsWholeRange",
+ save_before_dump=True,
+ )
+
+ def test_varlib_build_feature_variations_whole_range_empty(self):
+ """Designspace file contains <rules> element without a condition, specifying
+ the entire design space, used to build GSUB FeatureVariations table.
+ """
+ self._run_varlib_build_test(
+ designspace_name="FeatureVarsWholeRangeEmpty",
+ font_name="TestFamily",
+ tables=["fvar", "GSUB"],
+ expected_ttx_name="FeatureVarsWholeRange",
+ save_before_dump=True,
+ )
+
def test_varlib_build_feature_variations_with_existing_rclt(self):
"""Designspace file contains <rules> element, used to build GSUB
FeatureVariations table. <rules> is specified to do its OT processing
diff --git a/setup.cfg b/setup.cfg
index 848b795e..f7688f70 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 4.9.0
+current_version = 4.10.0
commit = True
tag = False
tag_name = {new_version}
@@ -34,6 +34,7 @@ license_file = LICENSE
minversion = 3.0
testpaths =
Tests
+ fontTools
python_files =
*_test.py
python_classes =
diff --git a/setup.py b/setup.py
index 74e134f2..01df358e 100755
--- a/setup.py
+++ b/setup.py
@@ -437,7 +437,7 @@ if ext_modules:
setup_params = dict(
name="fonttools",
- version="4.9.0",
+ version="4.10.0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",
diff --git a/tox.ini b/tox.ini
index c160e8b4..df6358c2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
[tox]
minversion = 3.0
envlist = py3{6,7,8}-cov, htmlcov
+skip_missing_interpreters=true
[testenv]
setenv =
@@ -21,7 +22,7 @@ commands =
cy: python -c "from fontTools.cu2qu.cu2qu import COMPILED; assert COMPILED"
!cy: python -c "from fontTools.cu2qu.cu2qu import COMPILED; assert not COMPILED"
# test with or without coverage, passing extra positonal args to pytest
- cov: coverage run --parallel-mode -m pytest {posargs:Tests fontTools}
+ cov: coverage run --parallel-mode -m pytest {posargs}
!cov: pytest {posargs:Tests fontTools}
[testenv:htmlcov]