diff --git a/Doc/docs-requirements.txt b/Doc/docs-requirements.txt index 9170d92ad..998172d5d 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 b9c419fe0..ab9f35678 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 84fb56fab..6e89857f4 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/colorLib/builder.rst b/Doc/source/colorLib/builder.rst deleted file mode 100644 index 94170f1d7..000000000 --- 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 c06224b23..000000000 --- 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 06721beac..d4eb9f83d 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 e0ee7a936..82a5d5799 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/developer.rst b/Doc/source/developer.rst new file mode 100644 index 000000000..0796eef7c --- /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 `_ 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 `__. +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 `__ 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 +--------------------- + +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 `_. 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 `_. 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 \ No newline at end of file diff --git a/Doc/source/encodings/StandardEncoding.rst b/Doc/source/encodings/StandardEncoding.rst deleted file mode 100644 index 4c936c05c..000000000 --- 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 ea0b03e9a..000000000 --- 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 6de20f4f7..32d13c70e 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 b56d5e754..000000000 --- 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 6804299e8..000000000 --- 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 8acbeada4..000000000 --- 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 fa85e0f59..000000000 --- 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 363761db8..61ac31f3b 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 `_. - 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 939abf7ef..000000000 --- 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 2d5a61245..000000000 --- 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 41f70c352..2162cc130 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 `_. Among other things this means you can use it free of charge. +The project has an `MIT open-source 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 `_ 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. +Utilities +--------- -TTX – From OpenType and TrueType to XML and Back ------------------------------------------------- +fontTools installs four command-line utilities: -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:: +- ``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. - ttx /path/to/font.otf - ttx /path/to/font.ttx +This last utility takes a subcommand, which could be one of: -The TTX application can be used in two ways, depending on what platform you run it on: +- ``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 -* As a command line tool (Windows/DOS, Unix, macOS) -* By dropping files onto the application (Windows, macOS) +Libraries +--------- -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``. +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: -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: +- :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 `_ of the fontTools repository. -* 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 - - >>> - -A selection of sample Python programs is in the `Snippets directory `_ 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 +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. +in some of the library modules. See the :doc:`optional requirements <./optional>` +page for more information. -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 `__. 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 `__: (aka ``pyfilesystem2``) filesystem abstraction layer. - -* `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 `__: 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 `__: ``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 `__: the Scientific Library for Python, which internally - uses `NumPy `__ arrays and hence is very fast; -* `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 `__: 2D plotting library. - -*Extra:* ``plot`` - - -:py:mod:`fontTools.misc.symfont` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Advanced module for symbolic font statistics analysis; it requires: - -* `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 `__: 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 `__: 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 `__: 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 `__: Python toolkit for generating PDFs and - graphics. - - -Testing -------- - -To run the test suite, you need to install `pytest `__. -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 `__ 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 `_. 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 2fd85ef28..311461557 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 000000000..09376a26a --- /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 `__. 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 `__: (aka ``pyfilesystem2``) filesystem abstraction layer. + +* `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 `__: 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 `__: ``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 `__: the Scientific Library for Python, which internally + uses `NumPy `__ arrays and hence is very fast; +* `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 `__: 2D plotting library. + +*Extra:* ``plot`` + + +:py:mod:`fontTools.misc.symfont` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Advanced module for symbolic font statistics analysis; it requires: + +* `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 `__: 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 `__: 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 `__: 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 `__: Python toolkit for generating PDFs and + graphics. diff --git a/Doc/source/ttx.rst b/Doc/source/ttx.rst index bdc938583..d672bfc80 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 88a0272f5..2bc4dab3e 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/afmLib.py b/Lib/fontTools/afmLib.py index 9b0c9e4fc..7983fa09b 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 `_, + 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 e47112aaf..b7d0bfa3a 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/width.py b/Lib/fontTools/cffLib/width.py index d959da1c5..fefae3aa8 100644 --- a/Lib/fontTools/cffLib/width.py +++ b/Lib/fontTools/cffLib/width.py @@ -146,17 +146,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 9fe7f203b..dae00b918 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/__main__.py b/Lib/fontTools/cu2qu/__main__.py index 32715062f..084bf8f96 100644 --- a/Lib/fontTools/cu2qu/__main__.py +++ b/Lib/fontTools/cu2qu/__main__.py @@ -1,4 +1,3 @@ -"""Convert a UFO font with cubic curves to quadratic curves""" import sys from .cli import main diff --git a/Lib/fontTools/cu2qu/cli.py b/Lib/fontTools/cu2qu/cli.py index 7f3c1ef7b..d4e83b883 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/errors.py b/Lib/fontTools/cu2qu/errors.py index d3adfea45..74c4c2271 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 81ba360ac..e7db157f0 100644 --- a/Lib/fontTools/feaLib/__main__.py +++ b/Lib/fontTools/feaLib/__main__.py @@ -1,4 +1,3 @@ -"""Add features from a feature file (.fea) into a OTF font""" from fontTools.misc.py23 import * from fontTools.ttLib import TTFont from fontTools.feaLib.builder import addOpenTypeFeatures, Builder @@ -14,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 e416c0c62..a305e67f7 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] @FRENCH_ACCENTS; -# markClass [cedilla] @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] @FRENCH_ACCENTS; + markClass [cedilla] @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] @FRENCH_ACCENTS; + # markClass [cedilla] @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 mark @TOP_MARKS + # ligComponent 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 `_. + """ 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. `_)""" 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 52b23f1b2..c1dc920e3 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 a3eaf6261..3a63c6e0c 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 [ mark @MARKCLASS]*.""" + # Parses a sequence of ``[ 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 `_). 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 `_). 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 `_) 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 `_). 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 `_. 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 `_.""" 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 `_) 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 `_) 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 `_.""" 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 `_. 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 `_. 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 d4b940512..f3fe92a83 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 index b707c9f09..ff8048d5b 100644 --- a/Lib/fontTools/help.py +++ b/Lib/fontTools/help.py @@ -1,32 +1,34 @@ -"""Show this help""" import pkgutil import sys -from setuptools import find_packages -from pkgutil import iter_modules import fontTools import importlib +import os +from pathlib import Path -def describe(pkg): - try: - description = __import__( - "fontTools." + pkg + ".__main__", globals(), locals(), ["__doc__"] - ).__doc__ - print("fonttools %-10s %s" % (pkg, description), file=sys.stderr) - except Exception as e: - return None - - -def show_help_list(): - path = fontTools.__path__[0] - for pkg in find_packages(path): - qualifiedPkg = "fontTools." + pkg - describe(pkg) - pkgpath = path + "/" + qualifiedPkg.replace(".", "/") - for info in iter_modules([pkgpath]): - describe(pkg + "." + info.name) +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) - show_help_list() + main() diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py index 9ef31f8ed..890234df9 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 e08aec989..d9c1dbb3e 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 b894653d4..5f1427d0d 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 59055062b..be9bc851d 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 f0f0a1336..4176fb253 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) - if font is None: - font = MockFont() + import argparse + parser = argparse.ArgumentParser( + "fonttools mtiLib", + description=main.__doc__, + ) - tableTag = None - if args[0].startswith('-t'): - tableTag = args[0][2:] - del args[0] - for f in args: + 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: + if args.font: + font = ttLib.TTFont(args.font) + else: + font = MockFont() + + 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/mtiLib/__main__.py b/Lib/fontTools/mtiLib/__main__.py index 0d4183b54..eacfefd59 100644 --- a/Lib/fontTools/mtiLib/__main__.py +++ b/Lib/fontTools/mtiLib/__main__.py @@ -1,8 +1,6 @@ -"""Convert Monotype FontDame layout files to TTX""" from fontTools.misc.py23 import * import sys from fontTools.mtiLib import main - if __name__ == '__main__': sys.exit(main()) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index dd0aabe75..4d9d2bc02 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 0119a92bd..7f8aba87f 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 e1d9f123a..93549d5d1 100644 --- a/Lib/fontTools/subset/__main__.py +++ b/Lib/fontTools/subset/__main__.py @@ -1,4 +1,3 @@ -"""OpenType font subsetter and optimizer""" from fontTools.misc.py23 import * import sys from fontTools.subset import main diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py index 9be149e40..9c45305d5 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 f71ae95e6..b065df078 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 cd323f657..944170713 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 de5442045..2cd479c9a 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 e9ff2151d..ec5d07eeb 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 849bf0ff7..e77ad9a4d 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 faacbedbe..9522c625f 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/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py index 460296977..404ad8b85 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 862decacd..1bf586f70 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 c147b4980..4b3a0f532 100644 --- a/Lib/fontTools/varLib/__main__.py +++ b/Lib/fontTools/varLib/__main__.py @@ -1,4 +1,3 @@ -"""Build a variable font from a designspace file and masters""" import sys from fontTools.varLib import main diff --git a/Lib/fontTools/varLib/cff.py b/Lib/fontTools/varLib/cff.py index 000e1b34a..4e2672b3b 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/instancer.py b/Lib/fontTools/varLib/instancer.py index f0cb646a0..2d22d622f 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 d4d7eeda7..6488022f6 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 d008e1cef..6d0385dd7 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 -instance.ttf)") + parser.add_argument('-l', '--loglevel', metavar='LEVEL', default="INFO", + help="Logging level (defaults to INFO)") - # TODO: allow user to configure logging via command-line options - configLogger(level="INFO") + + args = parser.parse_args(args) + + if not args.output: + args.output = os.path.splitext(args.designspace_filename)[0] + '-instance.ttf' + + 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 b7fb39b4c..071942b86 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 d6837ee62..eddf3b218 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 bd8882494..b5954f8b9 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 7239e9545..3d9566a1c 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/MetaTools/buildTableList.py b/MetaTools/buildTableList.py index 825f0db1a..36b9fa0a0 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 d02ec4a5e..f9094ab04 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/README.rst b/README.rst index dc961c2c3..2fade22ee 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,9 @@ What is this? licence `__. | Among other things this means you can use it free of charge. +`User documentation ` and +`developer documentation ` are available at `Read the Docs `. + Installation ~~~~~~~~~~~~ @@ -54,112 +57,6 @@ Python 3 `venv `__ 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 - - >>> - -A selection of sample Python programs is in the -`Snippets `__ -directory. - Optional Requirements --------------------- @@ -297,64 +194,6 @@ are required to unlock the extra features named "ufo", etc. * `reportlab `__: Python toolkit for generating PDFs and graphics. -Testing -~~~~~~~ - -To run the test suite, you need to install `pytest `__. -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 `__ 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 378effe6e..f883730fe 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 5ce4cc266..f2f1c05d6 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 000000000..e0c22226b --- /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 000000000..927694cbc --- /dev/null +++ b/Tests/feaLib/data/MultipleLookupsPerGlyph.ttx @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea b/Tests/feaLib/data/MultipleLookupsPerGlyph2.fea new file mode 100644 index 000000000..5a9d19b24 --- /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 000000000..008d95b65 --- /dev/null +++ b/Tests/feaLib/data/MultipleLookupsPerGlyph2.ttx @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py index d05a82448..87b8c96a4 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 bc1aae250..ed8fd3075 100644 --- a/Tests/fontBuilder/data/test_var.ttf.ttx +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -204,6 +204,9 @@ Right Up + + Neutral + HalloTestFont @@ -237,6 +240,9 @@ Right Up + + Neutral + HalloTestFont @@ -363,6 +369,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index bc7837c30..6368cb876 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 00e719b8c..0fb89c10e 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 3675395fe..727d685f3 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", [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ']), + ([ + 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, [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ']), + ([ + dict( + tag="wght", + name="Weight", + values=[ + dict(value=400, name='Regular', flags=0x2), + dict(value=600, linkedValue=650, name='Bold')])], None, 18, [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ']), + ([ + 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, [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ']), + ([ + 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, [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ']), +] + + +@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 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 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 000000000..899b037e3 --- /dev/null +++ b/Tests/subset/data/TestContextSubstFormat3.ttx @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (c) 2015-2019 Belleve Invis. + + + Iosevka Medium + + + Regular + + + Iosevka Medium Version 3.0.0-rc.8 + + + Iosevka Medium + + + Version 3.0.0-rc.8; ttfautohint (v1.8.3) + + + Iosevka-Medium + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/subset_test.py b/Tests/subset/subset_test.py index 2493daff1..2cce9baf5 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 6de6e2473..9a00fded0 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 = [ '', '', - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', '', ' ', ' ', @@ -113,6 +104,15 @@ GVAR_XML = [ ' ', ' ', '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '', ] diff --git a/Tests/ttLib/tables/_m_e_t_a_test.py b/Tests/ttLib/tables/_m_e_t_a_test.py index 4f0a41589..3a4f2f5b2 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): '' ], [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([ + '', + '', + '48656c6c 6f21', + '' + ], [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 d770a5231..5f5c965ce 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/varLib/data/test_results/Build.ttx b/Tests/varLib/data/test_results/Build.ttx index 6e9c6e37f..5a406c84d 100644 --- a/Tests/varLib/data/test_results/Build.ttx +++ b/Tests/varLib/data/test_results/Build.ttx @@ -405,509 +405,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1606,6 +1103,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx index 7e5d9561f..20add49ec 100644 --- a/Tests/varLib/data/test_results/BuildMain.ttx +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -1051,509 +1051,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2252,6 +1749,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/SparseMasters.ttx b/Tests/varLib/data/test_results/SparseMasters.ttx index c2aa335ce..fb9cb46d4 100644 --- a/Tests/varLib/data/test_results/SparseMasters.ttx +++ b/Tests/varLib/data/test_results/SparseMasters.ttx @@ -572,6 +572,19 @@ + + + + + + + + + + + + + @@ -614,6 +627,12 @@ + + + + + + @@ -635,25 +654,6 @@ - - - - - - - - - - - - - - - - - - -