Merge remote-tracking branch 'origin/master' into ufo-minor-format-version

This commit is contained in:
Cosimo Lupo 2020-05-14 11:01:39 +01:00
commit f4752fd412
No known key found for this signature in database
GPG Key ID: 179A8F0895A02F4F
85 changed files with 3884 additions and 1932 deletions

View File

@ -1,3 +1,3 @@
sphinx == 3.0.2
sphinx==3.0.3
sphinx_rtd_theme == 0.4.3
reportlab == 3.5.42

View File

@ -1,8 +1,8 @@
######
afmLib
######
###########################################
afmLib: Read/write Adobe Font Metrics files
###########################################
.. automodule:: fontTools.afmLib
:inherited-members:
.. autoclass:: fontTools.afmLib.AFM
:members:
:undoc-members:

View File

@ -1,8 +1,6 @@
###
agl
###
######################################
agl: Interface to the Adobe Glyph List
######################################
.. automodule:: fontTools.agl
:inherited-members:
:members:
:undoc-members:
:members: toUnicode, UV2AGL, AGL2UV

View File

@ -1,8 +0,0 @@
#######
builder
#######
.. automodule:: fontTools.colorLib.builder
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
######
errors
######
.. automodule:: fontTools.colorLib.errors
:inherited-members:
:members:
:undoc-members:

View File

@ -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:
:undoc-members:

View File

@ -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"]

115
Doc/source/developer.rst Normal file
View File

@ -0,0 +1,115 @@
.. _developerinfo:
.. image:: ../../Icons/FontToolsIconGreenCircle.png
:width: 200px
:height: 200px
:alt: Font Tools
:align: center
fontTools Developer Information
===============================
If you would like to contribute to the development of fontTools, you can clone the repository from GitHub, install the package in 'editable' mode and modify the source code in place. We recommend creating a virtual environment, using the Python 3 `venv <https://docs.python.org/3/library/venv.html>`_ module::
# download the source code to 'fonttools' folder
git clone https://github.com/fonttools/fonttools.git
cd fonttools
# create new virtual environment called e.g. 'fonttools-venv', or anything you like
python -m venv fonttools-venv
# source the `activate` shell script to enter the environment (Un*x)
. fonttools-venv/bin/activate
# to activate the virtual environment in Windows `cmd.exe`, do
fonttools-venv\Scripts\activate.bat
# install in 'editable' mode
pip install -e .
.. note::
To exit a Python virtual environment, enter the command ``deactivate``.
Testing
-------
To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
When you run the ``pytest`` command, the tests will run against the
installed fontTools package, or the first one found in the
``PYTHONPATH``.
You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
automatically run tests on different Python versions in isolated virtual
environments::
pip install tox
tox
.. note::
When you run ``tox`` without arguments, the tests are executed for all the environments listed in the ``tox.ini`` ``envlist``. The 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 <https://gitter.im/fonttools-dev/Lobby>`_. There is also a development https://groups.google.com/d/forum/fonttools-dev mailing list for continuous integration notifications.
History
-------
The fontTools project was started by Just van Rossum in 1999, and was
maintained as an open source project at
http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
began helping Just with stability maintenance. In 2013 Behdad Esfahbod
began a friendly fork, thoroughly reviewing the codebase and making
changes at https://github.com/behdad/fonttools to add new features and
support for new font formats.
Acknowledgments
---------------
In alphabetical order:
Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland,
Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent
Connare, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod,
Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis
Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson,
Denis Moyogo Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek,
Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo,
Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret
Rieger, Read Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel,
Georg Seifert, Chris Simpkins, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov,
Paul Wise.
License
-------
`MIT license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. See the full text of the license for details.
.. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg
:target: https://travis-ci.org/fonttools/fonttools
.. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true
:target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master
.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg
:target: https://codecov.io/gh/fonttools/fonttools
.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg
:target: https://pypi.org/project/FontTools
.. |Gitter Chat| image:: https://badges.gitter.im/fonttools-dev/Lobby.svg
:alt: Join the chat at https://gitter.im/fonttools-dev/Lobby
:target: https://gitter.im/fonttools-dev/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

View File

@ -1,10 +0,0 @@
################
StandardEncoding
################
.. automodule:: fontTools.encodings.StandardEncoding
:inherited-members:
:members:
:undoc-members:
.. data:: fontTools.encodings.StandardEncoding.StandardEncoding

View File

@ -1,8 +0,0 @@
######
codecs
######
.. automodule:: fontTools.encodings.codecs
:inherited-members:
:members:
:undoc-members:

View File

@ -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'

View File

@ -1,10 +0,0 @@
########
MacRoman
########
.. automodule:: fontTools.encodings.MacRoman
:inherited-members:
:members:
:undoc-members:
.. data:: fontTools.encodings.MacRoman.MacRoman

View File

@ -1,8 +0,0 @@
###
ast
###
.. automodule:: fontTools.feaLib.ast
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#######
builder
#######
.. automodule:: fontTools.feaLib.builder
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#####
error
#####
.. automodule:: fontTools.feaLib.error
:inherited-members:
:members:
:undoc-members:

View File

@ -1,17 +1,40 @@
######
feaLib
######
#########################################
feaLib: Read/write OpenType feature files
#########################################
.. toctree::
:maxdepth: 1
fontTools' ``feaLib`` allows for the creation and parsing of Adobe
Font Development Kit for OpenType feature (``.fea``) files. The syntax
of these files is described `here <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html>`_.
ast
builder
error
lexer
parser
The :class:`fontTools.feaLib.parser.Parser` class can be used to parse files
into an abstract syntax tree, and from there the
:class:`fontTools.feaLib.builder.Builder` class can add features to an existing
font file. You can inspect the parsed syntax tree, walk the tree and do clever
things with it, and also generate your own feature files programmatically, by
using the classes in the :mod:`fontTools.feaLib.ast` module.
.. automodule:: fontTools.feaLib
:inherited-members:
Parsing
-------
.. autoclass:: fontTools.feaLib.parser.Parser
:members: parse
:member-order: bysource
Building
---------
.. automodule:: fontTools.feaLib.builder
:members: addOpenTypeFeatures, addOpenTypeFeaturesFromString
Generation/Interrogation
------------------------
.. _`glyph-containing object`:
.. _`glyph-containing objects`:
In the below, a **glyph-containing object** is an object of one of the following
classes: :class:`GlyphName`, :class:`GlyphClass`, :class:`GlyphClassName`.
.. automodule:: fontTools.feaLib.ast
:member-order: bysource
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
#####
lexer
#####
.. automodule:: fontTools.feaLib.lexer
:inherited-members:
:members:
:undoc-members:

View File

@ -1,8 +0,0 @@
######
parser
######
.. automodule:: fontTools.feaLib.parser
:inherited-members:
:members:
:undoc-members:

View File

@ -8,13 +8,12 @@
fontTools Docs
==============
|Travis Build Status| |Appveyor Build status| |Coverage Status| |PyPI| |Gitter Chat|
About
-----
fontTools is a family of libraries and utilities for manipulating fonts in Python.
fontTools is a library for manipulating fonts, written in Python. The project includes the TTX tool, that can convert TrueType and OpenType fonts to and from an XML text format, which is also called TTX. It supports TrueType, OpenType, AFM and to an extent Type 1 and some Mac-specific formats. The project has an `MIT open-source license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. Among other things this means you can use it free of charge.
The project has an `MIT open-source license <https://github.com/fonttools/fonttools/blob/master/LICENSE>`_. Among other things this means you can use it free of charge.
Installation
------------
@ -27,322 +26,80 @@ The package is listed in the Python Package Index (PyPI), so you can install it
pip install fonttools
If you would like to contribute to its development, you can clone the repository from GitHub, install the package in 'editable' mode and modify the source code in place. We recommend creating a virtual environment, using the Python 3 `venv <https://docs.python.org/3/library/venv.html>`_ module::
# download the source code to 'fonttools' folder
git clone https://github.com/fonttools/fonttools.git
cd fonttools
# create new virtual environment called e.g. 'fonttools-venv', or anything you like
python -m venv fonttools-venv
# source the `activate` shell script to enter the environment (Un*x)
. fonttools-venv/bin/activate
# to activate the virtual environment in Windows `cmd.exe`, do
fonttools-venv\Scripts\activate.bat
# install in 'editable' mode
pip install -e .
.. note::
To exit a Python virtual environment, enter the command ``deactivate``.
See the Optional Requirements section below for details about module-specific dependencies that must be installed in select cases.
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 <https://github.com/fonttools/fonttools/blob/master/Snippets/>`_ 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
<fontTools.ttLib.TTFont object at 0x10c34ed50>
>>>
A selection of sample Python programs is in the `Snippets directory <https://github.com/fonttools/fonttools/blob/master/Snippets/>`_ of the fontTools repository.
Please navigate to the respective area of the documentation to learn more about the available modules in the fontTools library.
Optional Requirements
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 <https://lxml.de>`__. The latter is preferred whenever present, as it is generally faster and more secure.
*Extra:* ``lxml``
:py:mod:`fontTools.ufoLib`
^^^^^^^^^^^^^^^^^^^^^^^^^^
Package for reading and writing UFO source files; it requires:
* `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem abstraction layer.
* `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum`` module (only required on Python < 3.4).
*Extra:* ``ufo``
:py:mod:`fontTools.ttLib.woff2`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Module to compress/decompress WOFF 2.0 web fonts; it requires:
* `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of the Brotli compression library.
*Extra:* ``woff``
:py:mod:`fontTools.unicode`
^^^^^^^^^^^^^^^^^^^^^^^^^^^
To display the Unicode character names when dumping the ``cmap`` table
with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
The version included in there varies between different Python versions.
To use the latest available data, you can install:
* `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: ``unicodedata`` backport for Python 2.7
and 3.x updated to the latest Unicode version 12.0. Note this is not necessary if you use Python 3.8
as the latter already comes with an up-to-date ``unicodedata``.
*Extra:* ``unicode``
:py:mod:`fontTools.varLib.interpolatable`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Module for finding wrong contour/component order between different masters.
It requires one of the following packages in order to solve the so-called
"minimum weight perfect matching problem in bipartite graphs", or
the Assignment problem:
* `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library for Python, which internally
uses `NumPy <https://pypi.python.org/pypi/numpy>`__ arrays and hence is very fast;
* `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python module that implements the Hungarian
or Kuhn-Munkres algorithm.
*Extra:* ``interpolatable``
:py:mod:`fontTools.varLib.plot`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Module for visualizing DesignSpaceDocument and resulting VariationModel.
* `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
*Extra:* ``plot``
:py:mod:`fontTools.misc.symfont`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Advanced module for symbolic font statistics analysis; it requires:
* `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for symbolic mathematics.
*Extra:* ``symfont``
:py:mod:`fontTools.t1Lib`
^^^^^^^^^^^^^^^^^^^^^^^^^
To get the file creator and type of Macintosh PostScript Type 1 fonts
on Python 3 you need to install the following module, as the old ``MacOS``
module is no longer included in Mac Python:
* `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for extended filesystem attributes
(macOS platform only).
*Extra:* ``type1``
:py:mod:`fontTools.pens.cocoaPen`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
* `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between Python and the Objective-C
runtime (macOS platform only).
:py:mod:`fontTools.pens.qtPen`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
* `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for the Qt cross platform UI and
application toolkit.
:py:mod:`fontTools.pens.reportLabPen`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pen to drawing glyphs as PNG images, requires:
* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit for generating PDFs and
graphics.
Testing
-------
To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
When you run the ``pytest`` command, the tests will run against the
installed fontTools package, or the first one found in the
``PYTHONPATH``.
You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
automatically run tests on different Python versions in isolated virtual
environments::
pip install tox
tox
.. note::
When you run ``tox`` without arguments, the tests are executed for all the environments listed in the ``tox.ini`` ``envlist``. The current Python interpreters defined for tox testing must be available on your system ``PATH``.
You can specify a different testing environment list via the ``-e`` option, or the ``TOXENV`` environment variable::
tox -e py36
TOXENV="py36-cov,htmlcov" tox
Development Community
Developer information
---------------------
fontTools development is ongoing in an active community of developers that includes professional developers employed at major software corporations and type foundries as well as hobbyists.
Feature requests and bug reports are always welcome at https://github.com/fonttools/fonttools/issues/
The best place for end-user and developer discussion about the fontTools project is the `fontTools gitter channel <https://gitter.im/fonttools-dev/Lobby>`_. There is also a development https://groups.google.com/d/forum/fonttools-dev mailing list for continuous integration notifications.
History
-------
The fontTools project was started by Just van Rossum in 1999, and was
maintained as an open source project at
http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
began helping Just with stability maintenance. In 2013 Behdad Esfahbod
began a friendly fork, thoroughly reviewing the codebase and making
changes at https://github.com/behdad/fonttools to add new features and
support for new font formats.
Acknowledgments
---------------
In alphabetical order:
Olivier Berten, Samyak Bhuta, Erik van Blokland, Petr van Blokland,
Jelle Bosma, Sascha Brawer, Tom Byrer, Frédéric Coiffier, Vincent
Connare, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod,
Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Yannis
Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson,
Denis Moyogo Jacquerye, Jack Jansen, Tom Kacvinsky, Jens Kutilek,
Antoine Leca, Werner Lemberg, Tal Leming, Peter Lofting, Cosimo Lupo,
Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret
Rieger, Read Roberts, Guido van Rossum, Just van Rossum, Andreas Seidel,
Georg Seifert, Chris Simpkins, Miguel Sousa, Adam Twardoch, Adrien Tétar, Vitaly Volkov,
Paul Wise.
Information for developers can be found :doc:`here <./developer>`.
License
-------

View File

@ -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:

140
Doc/source/optional.rst Normal file
View File

@ -0,0 +1,140 @@
Optional Dependencies
=====================
The fonttools PyPI distribution also supports so-called "extras", i.e. a
set of keywords that describe a group of additional dependencies, which can be
used when installing via pip, or when specifying a requirement.
For example:
.. code:: sh
pip install fonttools[ufo,lxml,woff,unicode]
This command will install fonttools, as well as the optional dependencies that
are required to unlock the extra features named "ufo", etc.
.. note::
Optional dependencies are detailed by module in the list below with the ``Extra`` setting that automates ``pip`` dependency installation when this is supported.
:py:mod:`fontTools.misc.etree`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The module exports a ElementTree-like API for reading/writing XML files, and allows to use as the backend either the built-in ``xml.etree`` module or `lxml <https://lxml.de>`__. The latter is preferred whenever present, as it is generally faster and more secure.
*Extra:* ``lxml``
:py:mod:`fontTools.ufoLib`
^^^^^^^^^^^^^^^^^^^^^^^^^^
Package for reading and writing UFO source files; it requires:
* `fs <https://pypi.org/pypi/fs>`__: (aka ``pyfilesystem2``) filesystem abstraction layer.
* `enum34 <https://pypi.org/pypi/enum34>`__: backport for the built-in ``enum`` module (only required on Python < 3.4).
*Extra:* ``ufo``
:py:mod:`fontTools.ttLib.woff2`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Module to compress/decompress WOFF 2.0 web fonts; it requires:
* `brotli <https://pypi.python.org/pypi/Brotli>`__: Python bindings of the Brotli compression library.
*Extra:* ``woff``
:py:mod:`fontTools.unicode`
^^^^^^^^^^^^^^^^^^^^^^^^^^^
To display the Unicode character names when dumping the ``cmap`` table
with ``ttx`` we use the ``unicodedata`` module in the Standard Library.
The version included in there varies between different Python versions.
To use the latest available data, you can install:
* `unicodedata2 <https://pypi.python.org/pypi/unicodedata2>`__: ``unicodedata`` backport for Python 2.7
and 3.x updated to the latest Unicode version 12.0. Note this is not necessary if you use Python 3.8
as the latter already comes with an up-to-date ``unicodedata``.
*Extra:* ``unicode``
:py:mod:`fontTools.varLib.interpolatable`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Module for finding wrong contour/component order between different masters.
It requires one of the following packages in order to solve the so-called
"minimum weight perfect matching problem in bipartite graphs", or
the Assignment problem:
* `scipy <https://pypi.python.org/pypi/scipy>`__: the Scientific Library for Python, which internally
uses `NumPy <https://pypi.python.org/pypi/numpy>`__ arrays and hence is very fast;
* `munkres <https://pypi.python.org/pypi/munkres>`__: a pure-Python module that implements the Hungarian
or Kuhn-Munkres algorithm.
*Extra:* ``interpolatable``
:py:mod:`fontTools.varLib.plot`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Module for visualizing DesignSpaceDocument and resulting VariationModel.
* `matplotlib <https://pypi.org/pypi/matplotlib>`__: 2D plotting library.
*Extra:* ``plot``
:py:mod:`fontTools.misc.symfont`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Advanced module for symbolic font statistics analysis; it requires:
* `sympy <https://pypi.python.org/pypi/sympy>`__: the Python library for symbolic mathematics.
*Extra:* ``symfont``
:py:mod:`fontTools.t1Lib`
^^^^^^^^^^^^^^^^^^^^^^^^^
To get the file creator and type of Macintosh PostScript Type 1 fonts
on Python 3 you need to install the following module, as the old ``MacOS``
module is no longer included in Mac Python:
* `xattr <https://pypi.python.org/pypi/xattr>`__: Python wrapper for extended filesystem attributes
(macOS platform only).
*Extra:* ``type1``
:py:mod:`fontTools.pens.cocoaPen`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pen for drawing glyphs with Cocoa ``NSBezierPath``, requires:
* `PyObjC <https://pypi.python.org/pypi/pyobjc>`__: the bridge between Python and the Objective-C
runtime (macOS platform only).
:py:mod:`fontTools.pens.qtPen`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pen for drawing glyphs with Qt's ``QPainterPath``, requires:
* `PyQt5 <https://pypi.python.org/pypi/PyQt5>`__: Python bindings for the Qt cross platform UI and
application toolkit.
:py:mod:`fontTools.pens.reportLabPen`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pen to drawing glyphs as PNG images, requires:
* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit for generating PDFs and
graphics.

View File

@ -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:

View File

@ -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

View File

@ -1,8 +1,50 @@
"""Module for reading and writing AFM files."""
"""Module for reading and writing AFM (Adobe Font Metrics) files.
Note that this has been designed to read in AFM files generated by Fontographer
and has not been tested on many other files. In particular, it does not
implement the whole Adobe AFM specification [#f1]_ but, it should read most
"common" AFM files.
Here is an example of using `afmLib` to read, modify and write an AFM file:
>>> from fontTools.afmLib import AFM
>>> f = AFM("Tests/afmLib/data/TestAFM.afm")
>>>
>>> # Accessing a pair gets you the kern value
>>> f[("V","A")]
-60
>>>
>>> # Accessing a glyph name gets you metrics
>>> f["A"]
(65, 668, (8, -25, 660, 666))
>>> # (charnum, width, bounding box)
>>>
>>> # Accessing an attribute gets you metadata
>>> f.FontName
'TestFont-Regular'
>>> f.FamilyName
'TestFont'
>>> f.Weight
'Regular'
>>> f.XHeight
500
>>> f.Ascender
750
>>>
>>> # Attributes and items can also be set
>>> f[("A","V")] = -150 # Tighten kerning
>>> f.FontName = "TestFont Squished"
>>>
>>> # And the font written out again
>>> f.write("testfont-squished.afm")
.. rubric:: Footnotes
.. [#f1] `Adobe Technote 5004 <https://www.adobe.com/content/dam/acom/en/devnet/font/pdfs/5004.AFM_Spec.pdf>`_,
Adobe Font Metrics File Format Specification.
"""
# XXX reads AFM's generated by Fog, not tested with much else.
# It does not implement the full spec (Adobe Technote 5004, Adobe Font Metrics
# File Format Specification). Still, it should read most "common" AFM files.
from fontTools.misc.py23 import *
import re
@ -97,6 +139,11 @@ class AFM(object):
]
def __init__(self, path=None):
"""AFM file reader.
Instantiating an object with a path name will cause the file to be opened,
read, and parsed. Alternatively the path can be left unspecified, and a
file can be parsed later with the :meth:`read` method."""
self._attrs = {}
self._chars = {}
self._kerning = {}
@ -107,6 +154,7 @@ class AFM(object):
self.read(path)
def read(self, path):
"""Opens, reads and parses a file."""
lines = readlines(path)
for line in lines:
if not line.strip():
@ -189,6 +237,7 @@ class AFM(object):
self._composites[charname] = components
def write(self, path, sep='\r'):
"""Writes out an AFM font to the given path."""
import time
lines = [ "StartFontMetrics 2.0",
"Comment Generated by afmLib; at %s" % (
@ -258,24 +307,40 @@ class AFM(object):
writelines(path, lines, sep)
def has_kernpair(self, pair):
"""Returns `True` if the given glyph pair (specified as a tuple) exists
in the kerning dictionary."""
return pair in self._kerning
def kernpairs(self):
"""Returns a list of all kern pairs in the kerning dictionary."""
return list(self._kerning.keys())
def has_char(self, char):
"""Returns `True` if the given glyph exists in the font."""
return char in self._chars
def chars(self):
"""Returns a list of all glyph names in the font."""
return list(self._chars.keys())
def comments(self):
"""Returns all comments from the file."""
return self._comments
def addComment(self, comment):
"""Adds a new comment to the file."""
self._comments.append(comment)
def addComposite(self, glyphName, components):
"""Specifies that the glyph `glyphName` is made up of the given components.
The components list should be of the following form::
[
(glyphname, xOffset, yOffset),
...
]
"""
self._composites[glyphName] = components
def __getattr__(self, attr):

View File

@ -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

View File

@ -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()

View File

@ -1,3 +1,7 @@
"""
colorLib.builder: Build COLR/CPAL tables from scratch
"""
import collections
import copy
import enum

View File

@ -1,4 +1,3 @@
"""Convert a UFO font with cubic curves to quadratic curves"""
import sys
from .cli import main

View File

@ -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__)

View File

@ -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

View File

@ -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(

File diff suppressed because it is too large Load Diff

View File

@ -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):

View File

@ -12,6 +12,27 @@ log = logging.getLogger(__name__)
class Parser(object):
"""Initializes a Parser object.
Example:
.. code:: python
from fontTools.feaLib.parser import Parser
parser = Parser(file, font.getReverseGlyphMap())
parsetree = parser.parse()
Note: the ``glyphNames`` iterable serves a double role to help distinguish
glyph names from ranges in the presence of hyphens and to ensure that glyph
names referenced in a feature file are actually part of a font's glyph set.
If the iterable is left empty, no glyph name in glyph set checking takes
place, and all glyph tokens containing hyphens are treated as literal glyph
names, not as ranges. (Adding a space around the hyphen can, in any case,
help to disambiguate ranges from glyph names containing hyphens.)
By default, the parser will follow ``include()`` statements in the feature
file. To turn this off, pass ``followIncludes=False``.
"""
extensions = {}
ast = ast
SS_FEATURE_TAGS = {"ss%02d" % i for i in range(1, 20+1)}
@ -19,14 +40,7 @@ class Parser(object):
def __init__(self, featurefile, glyphNames=(), followIncludes=True,
**kwargs):
"""Initializes a Parser object.
Note: the `glyphNames` iterable serves a double role to help distinguish
glyph names from ranges in the presence of hyphens and to ensure that glyph
names referenced in a feature file are actually part of a font's glyph set.
If the iterable is left empty, no glyph name in glyph set checking takes
place.
"""
if "glyphMap" in kwargs:
from fontTools.misc.loggingTools import deprecateArgument
deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
@ -56,6 +70,9 @@ class Parser(object):
self.advance_lexer_(comments=True)
def parse(self):
"""Parse the file, and return a :class:`fontTools.feaLib.ast.FeatureFile`
object representing the root of the abstract syntax tree containing the
parsed contents of the file."""
statements = self.doc_.statements
while self.next_token_type_ is not None or self.cur_comments_:
self.advance_lexer_(comments=True)
@ -96,16 +113,18 @@ class Parser(object):
return self.doc_
def parse_anchor_(self):
# Parses an anchor in any of the four formats given in the feature
# file specification (2.e.vii).
self.expect_symbol_("<")
self.expect_keyword_("anchor")
location = self.cur_token_location_
if self.next_token_ == "NULL":
if self.next_token_ == "NULL": # Format D
self.expect_keyword_("NULL")
self.expect_symbol_(">")
return None
if self.next_token_type_ == Lexer.NAME:
if self.next_token_type_ == Lexer.NAME: # Format E
name = self.expect_name_()
anchordef = self.anchors_.resolve(name)
if anchordef is None:
@ -122,11 +141,11 @@ class Parser(object):
x, y = self.expect_number_(), self.expect_number_()
contourpoint = None
if self.next_token_ == "contourpoint":
if self.next_token_ == "contourpoint": # Format B
self.expect_keyword_("contourpoint")
contourpoint = self.expect_number_()
if self.next_token_ == "<":
if self.next_token_ == "<": # Format C
xDeviceTable = self.parse_device_()
yDeviceTable = self.parse_device_()
else:
@ -140,7 +159,7 @@ class Parser(object):
location=location)
def parse_anchor_marks_(self):
"""Parses a sequence of [<anchor> mark @MARKCLASS]*."""
# Parses a sequence of ``[<anchor> mark @MARKCLASS]*.``
anchorMarks = [] # [(self.ast.Anchor, markClassName)*]
while self.next_token_ == "<":
anchor = self.parse_anchor_()
@ -152,6 +171,7 @@ class Parser(object):
return anchorMarks
def parse_anchordef_(self):
# Parses a named anchor definition (`section 2.e.viii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.vii>`_).
assert self.is_cur_keyword_("anchorDef")
location = self.cur_token_location_
x, y = self.expect_number_(), self.expect_number_()
@ -168,6 +188,7 @@ class Parser(object):
return anchordef
def parse_anonymous_(self):
# Parses an anonymous data block (`section 10 <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#10>`_).
assert self.is_cur_keyword_(("anon", "anonymous"))
tag = self.expect_tag_()
_, content, location = self.lexer_.scan_anonymous_block(tag)
@ -179,6 +200,7 @@ class Parser(object):
return self.ast.AnonymousBlock(tag, content, location=location)
def parse_attach_(self):
# Parses a GDEF Attach statement (`section 9.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.b>`_)
assert self.is_cur_keyword_("Attach")
location = self.cur_token_location_
glyphs = self.parse_glyphclass_(accept_glyphname=True)
@ -190,12 +212,13 @@ class Parser(object):
location=location)
def parse_enumerate_(self, vertical):
# Parse an enumerated pair positioning rule (`section 6.b.ii <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#6.b.ii>`_).
assert self.cur_token_ in {"enumerate", "enum"}
self.advance_lexer_()
return self.parse_position_(enumerated=True, vertical=vertical)
def parse_GlyphClassDef_(self):
"""Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'"""
# Parses 'GlyphClassDef @BASE, @LIGATURES, @MARKS, @COMPONENTS;'
assert self.is_cur_keyword_("GlyphClassDef")
location = self.cur_token_location_
if self.next_token_ != ",":
@ -223,7 +246,7 @@ class Parser(object):
location=location)
def parse_glyphclass_definition_(self):
"""Parses glyph class definitions such as '@UPPERCASE = [A-Z];'"""
# Parses glyph class definitions such as '@UPPERCASE = [A-Z];'
location, name = self.cur_token_location_, self.cur_token_
self.expect_symbol_("=")
glyphs = self.parse_glyphclass_(accept_glyphname=False)
@ -273,6 +296,8 @@ class Parser(object):
location)
def parse_glyphclass_(self, accept_glyphname):
# Parses a glyph class, either named or anonymous, or (if
# ``bool(accept_glyphname)``) a glyph name.
if (accept_glyphname and
self.next_token_type_ in (Lexer.NAME, Lexer.CID)):
glyph = self.expect_glyph_()
@ -362,6 +387,7 @@ class Parser(object):
return glyphs
def parse_class_name_(self):
# Parses named class - either a glyph class or mark class.
name = self.expect_class_name_()
gc = self.glyphclasses_.resolve(name)
if gc is None:
@ -376,6 +402,11 @@ class Parser(object):
gc, location=self.cur_token_location_)
def parse_glyph_pattern_(self, vertical):
# Parses a glyph pattern, including lookups and context, e.g.::
#
# a b
# a b c' d e
# a b c' lookup ChangeC d e
prefix, glyphs, lookups, values, suffix = ([], [], [], [], [])
hasMarks = False
while self.next_token_ not in {"by", "from", ";", ","}:
@ -404,8 +435,10 @@ class Parser(object):
else:
values.append(None)
lookup = None
if self.next_token_ == "lookup":
lookuplist = None
while self.next_token_ == "lookup":
if lookuplist is None:
lookuplist = []
self.expect_keyword_("lookup")
if not marked:
raise FeatureLibError(
@ -417,8 +450,9 @@ class Parser(object):
raise FeatureLibError(
'Unknown lookup "%s"' % lookup_name,
self.cur_token_location_)
lookuplist.append(lookup)
if marked:
lookups.append(lookup)
lookups.append(lookuplist)
if not glyphs and not suffix: # eg., "sub f f i by"
assert lookups == []
@ -446,6 +480,7 @@ class Parser(object):
return chainContext, hasLookups
def parse_ignore_(self):
# Parses an ignore sub/pos rule.
assert self.is_cur_keyword_("ignore")
location = self.cur_token_location_
self.advance_lexer_()
@ -514,6 +549,8 @@ class Parser(object):
location=location)
def parse_lookup_(self, vertical):
# Parses a ``lookup`` - either a lookup block, or a lookup reference
# inside a feature.
assert self.is_cur_keyword_("lookup")
location, name = self.cur_token_location_, self.expect_name_()
@ -537,6 +574,8 @@ class Parser(object):
return block
def parse_lookupflag_(self):
# Parses a ``lookupflag`` statement, either specified by number or
# in words.
assert self.is_cur_keyword_("lookupflag")
location = self.cur_token_location_
@ -850,6 +889,8 @@ class Parser(object):
return self.ast.SubtableStatement(location=location)
def parse_size_parameters_(self):
# Parses a ``parameters`` statement used in ``size`` features. See
# `section 8.b <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.b>`_.
assert self.is_cur_keyword_("parameters")
location = self.cur_token_location_
DesignSize = self.expect_decipoint_()
@ -1003,6 +1044,7 @@ class Parser(object):
self.cur_token_location_)
def parse_name_(self):
"""Parses a name record. See `section 9.e <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.e>`_."""
platEncID = None
langID = None
if self.next_token_type_ in Lexer.NUMBERS:
@ -1130,6 +1172,7 @@ class Parser(object):
continue
def parse_base_tag_list_(self):
# Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
assert self.cur_token_ in ("HorizAxis.BaseTagList",
"VertAxis.BaseTagList"), self.cur_token_
bases = []
@ -1229,6 +1272,7 @@ class Parser(object):
vertical=vertical, location=location)
def parse_valuerecord_definition_(self, vertical):
# Parses a named value record definition. (See section `2.e.v <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#2.e.v>`_)
assert self.is_cur_keyword_("valueRecordDef")
location = self.cur_token_location_
value = self.parse_valuerecord_(vertical)
@ -1283,6 +1327,8 @@ class Parser(object):
location=location)
def parse_featureNames_(self, tag):
"""Parses a ``featureNames`` statement found in stylistic set features.
See section `8.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.c>`_."""
assert self.cur_token_ == "featureNames", self.cur_token_
block = self.ast.NestedBlock(tag, self.cur_token_,
location=self.cur_token_location_)
@ -1313,6 +1359,8 @@ class Parser(object):
return block
def parse_cvParameters_(self, tag):
# Parses a ``cvParameters`` block found in Character Variant features.
# See section `8.d <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#8.d>`_.
assert self.cur_token_ == "cvParameters", self.cur_token_
block = self.ast.NestedBlock(tag, self.cur_token_,
location=self.cur_token_location_)
@ -1388,6 +1436,8 @@ class Parser(object):
return self.ast.CharacterStatement(character, tag, location=location)
def parse_FontRevision_(self):
# Parses a ``FontRevision`` statement found in the head table. See
# `section 9.c <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.c>`_.
assert self.cur_token_ == "FontRevision", self.cur_token_
location, version = self.cur_token_location_, self.expect_float_()
self.expect_symbol_(";")

View File

@ -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)

View File

@ -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()

View File

@ -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:

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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!

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -1,4 +1,3 @@
"""OpenType font subsetter and optimizer"""
from fontTools.misc.py23 import *
import sys
from fontTools.subset import main

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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):

View File

@ -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',

View File

@ -1,4 +1,3 @@
"""Build a variable font from a designspace file and masters"""
import sys
from fontTools.varLib import main

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -58,29 +58,42 @@ def interpolate_layout(designspace, loc, master_finder=lambda s:s, mapped=False)
def main(args=None):
"""Interpolate GDEF/GPOS/GSUB tables for a point on a designspace"""
from fontTools import configLogger
import argparse
import sys
if args is None:
args = sys.argv[1:]
designspace_filename = args[0]
locargs = args[1:]
outfile = os.path.splitext(designspace_filename)[0] + '-instance.ttf'
parser = argparse.ArgumentParser(
"fonttools varLib.interpolate_layout",
description=main.__doc__,
)
parser.add_argument('designspace_filename', metavar='DESIGNSPACE',
help="Input TTF files")
parser.add_argument('locations', metavar='LOCATION', type=str, nargs='+',
help="Axis locations (e.g. wdth=120")
parser.add_argument('-o', '--output', metavar='OUTPUT',
help="Output font file (defaults to <designspacename>-instance.ttf)")
parser.add_argument('-l', '--loglevel', metavar='LEVEL', default="INFO",
help="Logging level (defaults to INFO)")
# 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__":

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -11,6 +11,9 @@ What is this?
licence <LICENSE>`__.
| Among other things this means you can use it free of charge.
`User documentation <https://fonttools.readthedocs.io/en/latest/>` and
`developer documentation <https://fonttools.readthedocs.io/en/latest/developer.html>` are available at `Read the Docs <https://fonttools.readthedocs.io/>`.
Installation
~~~~~~~~~~~~
@ -54,112 +57,6 @@ Python 3 `venv <https://docs.python.org/3/library/venv.html>`__ module.
# install in 'editable' mode
pip install -e .
TTX From OpenType and TrueType to XML and Back
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Once installed you can use the ``ttx`` command to convert binary font
files (``.otf``, ``.ttf``, etc) to the TTX XML format, edit them, and
convert them back to binary format. TTX files have a .ttx file
extension.
.. code:: sh
ttx /path/to/font.otf
ttx /path/to/font.ttx
The TTX application can be used in two ways, depending on what
platform you run it on:
- As a command line tool (Windows/DOS, Unix, macOS)
- By dropping files onto the application (Windows, macOS)
TTX detects what kind of files it is fed: it will output a ``.ttx`` file
when it sees a ``.ttf`` or ``.otf``, and it will compile a ``.ttf`` or
``.otf`` when the input file is a ``.ttx`` file. By default, the output
file is created in the same folder as the input file, and will have the
same name as the input file but with a different extension. TTX will
*never* overwrite existing files, but if necessary will append a unique
number to the output filename (before the extension) such as
``Arial#1.ttf``
When using TTX from the command line there are a bunch of extra options.
These are explained in the help text, as displayed when typing
``ttx -h`` at the command prompt. These additional options include:
- specifying the folder where the output files are created
- specifying which tables to dump or which tables to exclude
- merging partial ``.ttx`` files with existing ``.ttf`` or ``.otf``
files
- listing brief table info instead of dumping to ``.ttx``
- splitting tables to separate ``.ttx`` files
- disabling TrueType instruction disassembly
The TTX file format
-------------------
The following tables are currently supported:
.. begin table list
.. code::
BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, EBDT, EBLC, FFTM,
Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, LTSH,
MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, TSI1,
TSI2, TSI3, TSI5, TSIB, TSID, TSIJ, TSIP, TSIS, TSIV, TTFA, VDMX,
VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, cvt, feat, fpgm,
fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, hmtx, kern, lcar,
loca, ltag, maxp, meta, mort, morx, name, opbd, post, prep, prop,
sbix, trak, vhea and vmtx
.. end table list
Other tables are dumped as hexadecimal data.
TrueType fonts use glyph indices (GlyphIDs) to refer to glyphs in most
places. While this is fine in binary form, it is really hard to work
with for humans. Therefore we use names instead.
The glyph names are either extracted from the ``CFF`` table or the
``post`` table, or are derived from a Unicode ``cmap`` table. In the
latter case the Adobe Glyph List is used to calculate names based on
Unicode values. If all of these methods fail, names are invented based
on GlyphID (eg ``glyph00142``)
It is possible that different glyphs use the same name. If this happens,
we force the names to be unique by appending ``#n`` to the name (``n``
being an integer number.) The original names are being kept, so this has
no influence on a "round tripped" font.
Because the order in which glyphs are stored inside the binary font is
important, we maintain an ordered list of glyph names in the font.
Other Tools
~~~~~~~~~~~
Commands for merging and subsetting fonts are also available:
.. code:: sh
pyftmerge
pyftsubset
fontTools Python Module
~~~~~~~~~~~~~~~~~~~~~~~
The fontTools Python module provides a convenient way to
programmatically edit font files.
.. code:: py
>>> from fontTools.ttLib import TTFont
>>> font = TTFont('/path/to/font.ttf')
>>> font
<fontTools.ttLib.TTFont object at 0x10c34ed50>
>>>
A selection of sample Python programs is in the
`Snippets <https://github.com/fonttools/fonttools/blob/master/Snippets/>`__
directory.
Optional Requirements
---------------------
@ -297,64 +194,6 @@ are required to unlock the extra features named "ufo", etc.
* `reportlab <https://pypi.python.org/pypi/reportlab>`__: Python toolkit
for generating PDFs and graphics.
Testing
~~~~~~~
To run the test suite, you need to install `pytest <http://docs.pytest.org/en/latest/>`__.
When you run the ``pytest`` command, the tests will run against the
installed ``fontTools`` package, or the first one found in the
``PYTHONPATH``.
You can also use `tox <https://tox.readthedocs.io/en/latest/>`__ to
automatically run tests on different Python versions in isolated virtual
environments.
.. code:: sh
pip install tox
tox
Note that when you run ``tox`` without arguments, the tests are executed
for all the environments listed in tox.ini's ``envlist``. In our case,
this includes Python 3.6 and 3.7, so for this to work the ``python3.6``
and ``python3.7`` executables must be available in your ``PATH``.
You can specify an alternative environment list via the ``-e`` option,
or the ``TOXENV`` environment variable:
.. code:: sh
tox -e py36
TOXENV="py36-cov,htmlcov" tox
Development Community
~~~~~~~~~~~~~~~~~~~~~
TTX/FontTools development is ongoing in an active community of
developers, that includes professional developers employed at major
software corporations and type foundries as well as hobbyists.
Feature requests and bug reports are always welcome at
https://github.com/fonttools/fonttools/issues/
The best place for discussions about TTX from an end-user perspective as
well as TTX/FontTools development is the
https://groups.google.com/d/forum/fonttools mailing list. There is also
a development https://groups.google.com/d/forum/fonttools-dev mailing
list for continuous integration notifications. You can also email Behdad
privately at behdad@behdad.org
History
~~~~~~~
The fontTools project was started by Just van Rossum in 1999, and was
maintained as an open source project at
http://sourceforge.net/projects/fonttools/. In 2008, Paul Wise (pabs3)
began helping Just with stability maintenance. In 2013 Behdad Esfahbod
began a friendly fork, thoroughly reviewing the codebase and making
changes at https://github.com/behdad/fonttools to add new features and
support for new font formats.
Acknowledgements
~~~~~~~~~~~~~~~~

View File

@ -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")

View File

@ -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):

View File

@ -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;

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GSUB>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="2"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=3 -->
<Lookup index="0">
<LookupType value="2"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<MultipleSubst index="0">
<Substitution in="a" out="b,c"/>
</MultipleSubst>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0">
<Substitution in="b" out="d"/>
</SingleSubst>
</Lookup>
<Lookup index="2">
<LookupType value="6"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="b"/>
</LookAheadCoverage>
<!-- SubstCount=2 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</SubstLookupRecord>
</ChainContextSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>

View File

@ -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;

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont>
<GPOS>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="test"/>
<Feature>
<!-- LookupCount=1 -->
<LookupListIndex index="0" value="2"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=3 -->
<Lookup index="0">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="a"/>
</Coverage>
<ValueFormat value="5"/>
<Value XPlacement="-80" XAdvance="-160"/>
</SinglePos>
</Lookup>
<Lookup index="1">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SinglePos index="0" Format="1">
<Coverage>
<Glyph value="a"/>
</Coverage>
<ValueFormat value="2"/>
<Value YPlacement="100"/>
</SinglePos>
</Lookup>
<Lookup index="2">
<LookupType value="8"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextPos index="0" Format="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=1 -->
<InputCoverage index="0">
<Glyph value="a"/>
</InputCoverage>
<!-- LookAheadGlyphCount=1 -->
<LookAheadCoverage index="0">
<Glyph value="b"/>
</LookAheadCoverage>
<!-- PosCount=2 -->
<PosLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="0"/>
</PosLookupRecord>
<PosLookupRecord index="1">
<SequenceIndex value="0"/>
<LookupListIndex value="1"/>
</PosLookupRecord>
</ChainContextPos>
</Lookup>
</LookupList>
</GPOS>
</ttFont>

View File

@ -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(

View File

@ -204,6 +204,9 @@
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
Right Up
</namerecord>
<namerecord nameID="262" platformID="1" platEncID="0" langID="0x0" unicode="True">
Neutral
</namerecord>
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
HalloTestFont
</namerecord>
@ -237,6 +240,9 @@
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
Right Up
</namerecord>
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
Neutral
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
HalloTestFont
</namerecord>
@ -363,6 +369,86 @@
</FeatureVariations>
</GSUB>
<STAT>
<Version value="0x00010001"/>
<DesignAxisRecordSize value="8"/>
<!-- DesignAxisCount=4 -->
<DesignAxisRecord>
<Axis index="0">
<AxisTag value="LEFT"/>
<AxisNameID value="256"/> <!-- Left -->
<AxisOrdering value="0"/>
</Axis>
<Axis index="1">
<AxisTag value="RGHT"/>
<AxisNameID value="257"/> <!-- Right -->
<AxisOrdering value="1"/>
</Axis>
<Axis index="2">
<AxisTag value="UPPP"/>
<AxisNameID value="258"/> <!-- Up -->
<AxisOrdering value="2"/>
</Axis>
<Axis index="3">
<AxisTag value="DOWN"/>
<AxisNameID value="259"/> <!-- Down -->
<AxisOrdering value="3"/>
</Axis>
</DesignAxisRecord>
<!-- AxisValueCount=8 -->
<AxisValueArray>
<AxisValue index="0" Format="1">
<AxisIndex value="0"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="1" Format="1">
<AxisIndex value="0"/>
<Flags value="0"/>
<ValueNameID value="256"/> <!-- Left -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="2" Format="1">
<AxisIndex value="1"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="3" Format="1">
<AxisIndex value="1"/>
<Flags value="0"/>
<ValueNameID value="257"/> <!-- Right -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="4" Format="1">
<AxisIndex value="2"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="5" Format="1">
<AxisIndex value="2"/>
<Flags value="0"/>
<ValueNameID value="258"/> <!-- Up -->
<Value value="100.0"/>
</AxisValue>
<AxisValue index="6" Format="1">
<AxisIndex value="3"/>
<Flags value="2"/>
<ValueNameID value="262"/> <!-- Neutral -->
<Value value="0.0"/>
</AxisValue>
<AxisValue index="7" Format="1">
<AxisIndex value="3"/>
<Flags value="0"/>
<ValueNameID value="259"/> <!-- Down -->
<Value value="100.0"/>
</AxisValue>
</AxisValueArray>
<ElidedFallbackNameID value="2"/> <!-- TotallyNormal -->
</STAT>
<fvar>
<!-- Left -->

View File

@ -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()

View File

@ -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())

View File

@ -1,5 +1,9 @@
import io
import struct
from fontTools.misc.fixedTools import floatToFixed
from fontTools.misc.testTools import getXML
from fontTools.otlLib import builder
from fontTools import ttLib
from fontTools.ttLib.tables import otTables
import pytest
@ -1106,6 +1110,291 @@ class ClassDefBuilderTest(object):
assert not b.canAdd({"f"})
buildStatTable_test_data = [
([
dict(
tag="wght",
name="Weight",
values=[
dict(value=100, name='Thin'),
dict(value=400, name='Regular', flags=0x2),
dict(value=900, name='Black')])], None, "Regular", [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=1 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="257"/> <!-- Weight -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=3 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="258"/> <!-- Thin -->',
' <Value value="100.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="256"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
' <AxisValue index="2" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Black -->',
' <Value value="900.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="256"/> <!-- Regular -->',
' </STAT>']),
([
dict(
tag="wght",
name=dict(en="Weight", nl="Gewicht"),
values=[
dict(value=100, name=dict(en='Thin', nl='Dun')),
dict(value=400, name='Regular', flags=0x2),
dict(value=900, name='Black'),
]),
dict(
tag="wdth",
name="Width",
values=[
dict(value=50, name='Condensed'),
dict(value=100, name='Regular', flags=0x2),
dict(value=200, name='Extended')])], None, 2, [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=2 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="256"/> <!-- Weight -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' <Axis index="1">',
' <AxisTag value="wdth"/>',
' <AxisNameID value="260"/> <!-- Width -->',
' <AxisOrdering value="1"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=6 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="257"/> <!-- Thin -->',
' <Value value="100.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
' <AxisValue index="2" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Black -->',
' <Value value="900.0"/>',
' </AxisValue>',
' <AxisValue index="3" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="0"/>',
' <ValueNameID value="261"/> <!-- Condensed -->',
' <Value value="50.0"/>',
' </AxisValue>',
' <AxisValue index="4" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="100.0"/>',
' </AxisValue>',
' <AxisValue index="5" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="0"/>',
' <ValueNameID value="262"/> <!-- Extended -->',
' <Value value="200.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
' </STAT>']),
([
dict(
tag="wght",
name="Weight",
values=[
dict(value=400, name='Regular', flags=0x2),
dict(value=600, linkedValue=650, name='Bold')])], None, 18, [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=1 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="256"/> <!-- Weight -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=2 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="1">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="257"/> <!-- Regular -->',
' <Value value="400.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="3">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="258"/> <!-- Bold -->',
' <Value value="600.0"/>',
' <LinkedValue value="650.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
' </STAT>']),
([
dict(
tag="opsz",
name="Optical Size",
values=[
dict(nominalValue=6, rangeMaxValue=10, name='Small'),
dict(rangeMinValue=10, nominalValue=14, rangeMaxValue=24, name='Text', flags=0x2),
dict(rangeMinValue=24, nominalValue=600, name='Display')])], None, 2, [
' <STAT>',
' <Version value="0x00010001"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=1 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="opsz"/>',
' <AxisNameID value="256"/> <!-- Optical Size -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=3 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="257"/> <!-- Small -->',
' <NominalValue value="6.0"/>',
' <RangeMinValue value="-32768.0"/>',
' <RangeMaxValue value="10.0"/>',
' </AxisValue>',
' <AxisValue index="1" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Text -->',
' <NominalValue value="14.0"/>',
' <RangeMinValue value="10.0"/>',
' <RangeMaxValue value="24.0"/>',
' </AxisValue>',
' <AxisValue index="2" Format="2">',
' <AxisIndex value="0"/>',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Display -->',
' <NominalValue value="600.0"/>',
' <RangeMinValue value="24.0"/>',
' <RangeMaxValue value="32767.99998"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="2"/> <!-- missing from name table -->',
' </STAT>']),
([
dict(
tag="wght",
name="Weight",
ordering=1,
values=[]),
dict(
tag="ABCD",
name="ABCDTest",
ordering=0,
values=[
dict(value=100, name="Regular", flags=0x2)])],
[dict(location=dict(wght=300, ABCD=100), name='Regular ABCD')], 18, [
' <STAT>',
' <Version value="0x00010002"/>',
' <DesignAxisRecordSize value="8"/>',
' <!-- DesignAxisCount=2 -->',
' <DesignAxisRecord>',
' <Axis index="0">',
' <AxisTag value="wght"/>',
' <AxisNameID value="256"/> <!-- Weight -->',
' <AxisOrdering value="1"/>',
' </Axis>',
' <Axis index="1">',
' <AxisTag value="ABCD"/>',
' <AxisNameID value="257"/> <!-- ABCDTest -->',
' <AxisOrdering value="0"/>',
' </Axis>',
' </DesignAxisRecord>',
' <!-- AxisValueCount=2 -->',
' <AxisValueArray>',
' <AxisValue index="0" Format="4">',
' <!-- AxisCount=2 -->',
' <Flags value="0"/>',
' <ValueNameID value="259"/> <!-- Regular ABCD -->',
' <AxisValueRecord index="0">',
' <AxisIndex value="0"/>',
' <Value value="300.0"/>',
' </AxisValueRecord>',
' <AxisValueRecord index="1">',
' <AxisIndex value="1"/>',
' <Value value="100.0"/>',
' </AxisValueRecord>',
' </AxisValue>',
' <AxisValue index="1" Format="1">',
' <AxisIndex value="1"/>',
' <Flags value="2"/>',
' <ValueNameID value="258"/> <!-- Regular -->',
' <Value value="100.0"/>',
' </AxisValue>',
' </AxisValueArray>',
' <ElidedFallbackNameID value="18"/> <!-- missing from name table -->',
' </STAT>']),
]
@pytest.mark.parametrize("axes, axisValues, elidedFallbackName, expected_ttx", buildStatTable_test_data)
def test_buildStatTable(axes, axisValues, elidedFallbackName, expected_ttx):
font = ttLib.TTFont()
font["name"] = ttLib.newTable("name")
font["name"].names = []
builder.buildStatTable(font, axes, axisValues, elidedFallbackName)
f = io.StringIO()
font.saveXML(f, tables=["STAT"])
ttx = f.getvalue().splitlines()
ttx = ttx[3:-2] # strip XML header and <ttFont> element
assert expected_ttx == ttx
# Compile and round-trip
f = io.BytesIO()
font.save(f)
font = ttLib.TTFont(f)
f = io.StringIO()
font.saveXML(f, tables=["STAT"])
ttx = f.getvalue().splitlines()
ttx = ttx[3:-2] # strip XML header and <ttFont> element
assert expected_ttx == ttx
def test_stat_infinities():
negInf = floatToFixed(builder.AXIS_VALUE_NEGATIVE_INFINITY, 16)
assert struct.pack(">l", negInf) == b"\x80\x00\x00\x00"
posInf = floatToFixed(builder.AXIS_VALUE_POSITIVE_INFINITY, 16)
assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff"
if __name__ == "__main__":
import sys

View File

@ -0,0 +1,610 @@
<?xml version="1.0" encoding="UTF-8"?>
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9">
<GlyphOrder>
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
<GlyphID id="0" name=".notdef"/>
<GlyphID id="1" name="plus"/>
<GlyphID id="2" name="glyph00002"/>
<GlyphID id="3" name="glyph00003"/>
<GlyphID id="4" name="glyph00004"/>
<GlyphID id="5" name="glyph00005"/>
<GlyphID id="6" name="glyph00006"/>
<GlyphID id="7" name="glyph00007"/>
</GlyphOrder>
<head>
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="1.0"/>
<fontRevision value="1.0"/>
<checkSumAdjustment value="0xa69ed898"/>
<magicNumber value="0x5f0f3cf5"/>
<flags value="00000000 00001111"/>
<unitsPerEm value="1000"/>
<created value="Mon Nov 21 06:10:39 2016"/>
<modified value="Fri Apr 24 05:31:23 2020"/>
<xMin value="-1000"/>
<yMin value="-509"/>
<xMax value="1135"/>
<yMax value="1194"/>
<macStyle value="00000000 00000000"/>
<lowestRecPPEM value="8"/>
<fontDirectionHint value="0"/>
<indexToLocFormat value="0"/>
<glyphDataFormat value="0"/>
</head>
<hhea>
<tableVersion value="0x00010000"/>
<ascent value="977"/>
<descent value="-205"/>
<lineGap value="67"/>
<advanceWidthMax value="1000"/>
<minLeftSideBearing value="-1000"/>
<minRightSideBearing value="-1000"/>
<xMaxExtent value="1135"/>
<caretSlopeRise value="1"/>
<caretSlopeRun value="0"/>
<caretOffset value="0"/>
<reserved0 value="0"/>
<reserved1 value="0"/>
<reserved2 value="0"/>
<reserved3 value="0"/>
<metricDataFormat value="0"/>
<numberOfHMetrics value="1"/>
</hhea>
<maxp>
<!-- Most of this table will be recalculated by the compiler -->
<tableVersion value="0x10000"/>
<numGlyphs value="8"/>
<maxPoints value="240"/>
<maxContours value="41"/>
<maxCompositePoints value="163"/>
<maxCompositeContours value="12"/>
<maxZones value="1"/>
<maxTwilightPoints value="0"/>
<maxStorage value="0"/>
<maxFunctionDefs value="0"/>
<maxInstructionDefs value="0"/>
<maxStackElements value="0"/>
<maxSizeOfInstructions value="0"/>
<maxComponentElements value="4"/>
<maxComponentDepth value="3"/>
</maxp>
<OS_2>
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
will be recalculated by the compiler -->
<version value="4"/>
<xAvgCharWidth value="500"/>
<usWeightClass value="500"/>
<usWidthClass value="5"/>
<fsType value="00000000 00000000"/>
<ySubscriptXSize value="665"/>
<ySubscriptYSize value="716"/>
<ySubscriptXOffset value="0"/>
<ySubscriptYOffset value="143"/>
<ySuperscriptXSize value="0"/>
<ySuperscriptYSize value="0"/>
<ySuperscriptXOffset value="0"/>
<ySuperscriptYOffset value="0"/>
<yStrikeoutSize value="51"/>
<yStrikeoutPosition value="265"/>
<sFamilyClass value="2057"/>
<panose>
<bFamilyType value="2"/>
<bSerifStyle value="0"/>
<bWeight value="6"/>
<bProportion value="9"/>
<bContrast value="0"/>
<bStrokeVariation value="0"/>
<bArmStyle value="0"/>
<bLetterForm value="0"/>
<bMidline value="0"/>
<bXHeight value="0"/>
</panose>
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
<achVendID value="BE5N"/>
<fsSelection value="00000000 11000000"/>
<usFirstCharIndex value="43"/>
<usLastCharIndex value="43"/>
<sTypoAscender value="977"/>
<sTypoDescender value="-272"/>
<sTypoLineGap value="0"/>
<usWinAscent value="977"/>
<usWinDescent value="272"/>
<ulCodePageRange1 value="00100000 00000000 00000001 00011111"/>
<ulCodePageRange2 value="11000100 00000000 00000000 00000000"/>
<sxHeight value="530"/>
<sCapHeight value="735"/>
<usDefaultChar value="0"/>
<usBreakChar value="32"/>
<usMaxContext value="8"/>
</OS_2>
<hmtx>
<mtx name=".notdef" width="500" lsb="57"/>
<mtx name="glyph00002" width="500" lsb="57"/>
<mtx name="glyph00003" width="500" lsb="57"/>
<mtx name="glyph00004" width="500" lsb="-8"/>
<mtx name="glyph00005" width="500" lsb="-8"/>
<mtx name="glyph00006" width="500" lsb="-8"/>
<mtx name="glyph00007" width="500" lsb="-65"/>
<mtx name="plus" width="500" lsb="57"/>
</hmtx>
<cmap>
<tableVersion version="0"/>
<cmap_format_4 platformID="0" platEncID="3" language="0">
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
</cmap_format_4>
<cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="28" language="0" nGroups="1">
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
</cmap_format_12>
<cmap_format_4 platformID="3" platEncID="1" language="0">
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
</cmap_format_4>
<cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="28" language="0" nGroups="1">
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
</cmap_format_12>
</cmap>
<loca>
<!-- The 'loca' table will be calculated by the compiler -->
</loca>
<glyf>
<!-- The xMin, yMin, xMax and yMax values
will be recalculated by the compiler. -->
<TTGlyph name=".notdef"/><!-- contains no outline data -->
<TTGlyph name="glyph00002" xMin="57" yMin="139" xMax="508" yMax="541">
<contour>
<pt x="203" y="139" on="1"/>
<pt x="203" y="298" on="1"/>
<pt x="57" y="298" on="1"/>
<pt x="57" y="382" on="1"/>
<pt x="203" y="382" on="1"/>
<pt x="203" y="541" on="1"/>
<pt x="297" y="541" on="1"/>
<pt x="297" y="382" on="1"/>
<pt x="508" y="382" on="1"/>
<pt x="508" y="298" on="1"/>
<pt x="297" y="298" on="1"/>
<pt x="297" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="glyph00003" xMin="57" yMin="139" xMax="508" yMax="541">
<contour>
<pt x="260" y="139" on="1"/>
<pt x="260" y="298" on="1"/>
<pt x="57" y="298" on="1"/>
<pt x="57" y="382" on="1"/>
<pt x="260" y="382" on="1"/>
<pt x="260" y="541" on="1"/>
<pt x="354" y="541" on="1"/>
<pt x="354" y="382" on="1"/>
<pt x="508" y="382" on="1"/>
<pt x="508" y="298" on="1"/>
<pt x="354" y="298" on="1"/>
<pt x="354" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="glyph00004" xMin="-8" yMin="139" xMax="508" yMax="541">
<contour>
<pt x="203" y="139" on="1"/>
<pt x="203" y="298" on="1"/>
<pt x="-8" y="298" on="1"/>
<pt x="-8" y="382" on="1"/>
<pt x="203" y="382" on="1"/>
<pt x="203" y="541" on="1"/>
<pt x="297" y="541" on="1"/>
<pt x="297" y="382" on="1"/>
<pt x="508" y="382" on="1"/>
<pt x="508" y="298" on="1"/>
<pt x="297" y="298" on="1"/>
<pt x="297" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="glyph00005" xMin="-8" yMin="139" xMax="443" yMax="541">
<contour>
<pt x="203" y="139" on="1"/>
<pt x="203" y="298" on="1"/>
<pt x="-8" y="298" on="1"/>
<pt x="-8" y="382" on="1"/>
<pt x="203" y="382" on="1"/>
<pt x="203" y="541" on="1"/>
<pt x="297" y="541" on="1"/>
<pt x="297" y="382" on="1"/>
<pt x="443" y="382" on="1"/>
<pt x="443" y="298" on="1"/>
<pt x="297" y="298" on="1"/>
<pt x="297" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="glyph00006" xMin="-8" yMin="139" xMax="443" yMax="541">
<contour>
<pt x="146" y="139" on="1"/>
<pt x="146" y="298" on="1"/>
<pt x="-8" y="298" on="1"/>
<pt x="-8" y="382" on="1"/>
<pt x="146" y="382" on="1"/>
<pt x="146" y="541" on="1"/>
<pt x="240" y="541" on="1"/>
<pt x="240" y="382" on="1"/>
<pt x="443" y="382" on="1"/>
<pt x="443" y="298" on="1"/>
<pt x="240" y="298" on="1"/>
<pt x="240" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="glyph00007" xMin="-65" yMin="139" xMax="443" yMax="541">
<contour>
<pt x="203" y="139" on="1"/>
<pt x="203" y="298" on="1"/>
<pt x="-65" y="298" on="1"/>
<pt x="-65" y="382" on="1"/>
<pt x="203" y="382" on="1"/>
<pt x="203" y="541" on="1"/>
<pt x="297" y="541" on="1"/>
<pt x="297" y="382" on="1"/>
<pt x="443" y="382" on="1"/>
<pt x="443" y="298" on="1"/>
<pt x="297" y="298" on="1"/>
<pt x="297" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
<TTGlyph name="plus" xMin="57" yMin="139" xMax="443" yMax="541">
<contour>
<pt x="203" y="139" on="1"/>
<pt x="203" y="298" on="1"/>
<pt x="57" y="298" on="1"/>
<pt x="57" y="382" on="1"/>
<pt x="203" y="382" on="1"/>
<pt x="203" y="541" on="1"/>
<pt x="297" y="541" on="1"/>
<pt x="297" y="382" on="1"/>
<pt x="443" y="382" on="1"/>
<pt x="443" y="298" on="1"/>
<pt x="297" y="298" on="1"/>
<pt x="297" y="139" on="1"/>
</contour>
<instructions/>
</TTGlyph>
</glyf>
<name>
<namerecord nameID="0" platformID="3" platEncID="1" langID="0x409">
Copyright (c) 2015-2019 Belleve Invis.
</namerecord>
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
Iosevka Medium
</namerecord>
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
Regular
</namerecord>
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
Iosevka Medium Version 3.0.0-rc.8
</namerecord>
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
Iosevka Medium
</namerecord>
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
Version 3.0.0-rc.8; ttfautohint (v1.8.3)
</namerecord>
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
Iosevka-Medium
</namerecord>
</name>
<post>
<formatType value="3.0"/>
<italicAngle value="0.0"/>
<underlinePosition value="-50"/>
<underlineThickness value="50"/>
<isFixedPitch value="1"/>
<minMemType42 value="0"/>
<maxMemType42 value="6380"/>
<minMemType1 value="0"/>
<maxMemType1 value="1"/>
</post>
<gasp>
<gaspRange rangeMaxPPEM="65535" rangeGaspBehavior="15"/>
</gasp>
<GDEF>
<Version value="0x00010000"/>
<GlyphClassDef Format="2">
<ClassDef glyph=".notdef" class="1"/>
<ClassDef glyph="glyph00002" class="1"/>
<ClassDef glyph="glyph00003" class="1"/>
<ClassDef glyph="glyph00004" class="1"/>
<ClassDef glyph="glyph00005" class="1"/>
<ClassDef glyph="glyph00006" class="1"/>
<ClassDef glyph="glyph00007" class="1"/>
<ClassDef glyph="plus" class="1"/>
</GlyphClassDef>
</GDEF>
<GPOS>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=1 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=0 -->
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=0 -->
</FeatureList>
<LookupList>
<!-- LookupCount=0 -->
</LookupList>
</GPOS>
<GSUB>
<Version value="0x00010000"/>
<ScriptList>
<!-- ScriptCount=4 -->
<ScriptRecord index="0">
<ScriptTag value="DFLT"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="1">
<ScriptTag value="cyrl"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="2">
<ScriptTag value="grek"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
<ScriptRecord index="3">
<ScriptTag value="latn"/>
<Script>
<DefaultLangSys>
<ReqFeatureIndex value="65535"/>
<!-- FeatureCount=1 -->
<FeatureIndex index="0" value="0"/>
</DefaultLangSys>
<!-- LangSysCount=0 -->
</Script>
</ScriptRecord>
</ScriptList>
<FeatureList>
<!-- FeatureCount=1 -->
<FeatureRecord index="0">
<FeatureTag value="calt"/>
<Feature>
<!-- LookupCount=2 -->
<LookupListIndex index="0" value="0"/>
<LookupListIndex index="1" value="1"/>
</Feature>
</FeatureRecord>
</FeatureList>
<LookupList>
<!-- LookupCount=6 -->
<Lookup index="0">
<LookupType value="6"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<ChainContextSubst index="0" Format="2">
<Coverage Format="1">
<Glyph value="plus"/>
</Coverage>
<BacktrackClassDef Format="1">
<ClassDef glyph="glyph00005" class="1"/>
<ClassDef glyph="glyph00007" class="1"/>
</BacktrackClassDef>
<InputClassDef Format="1">
<ClassDef glyph="plus" class="1"/>
</InputClassDef>
<LookAheadClassDef Format="2">
</LookAheadClassDef>
<!-- ChainSubClassSetCount=2 -->
<ChainSubClassSet index="0" empty="1"/>
<ChainSubClassSet index="1">
<!-- ChainSubClassRuleCount=4 -->
<ChainSubClassRule index="0">
<!-- BacktrackGlyphCount=1 -->
<Backtrack index="0" value="1"/>
<!-- InputGlyphCount=1 -->
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=1 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="5"/>
</SubstLookupRecord>
</ChainSubClassRule>
<ChainSubClassRule index="1">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=4 -->
<Input index="0" value="1"/>
<Input index="1" value="1"/>
<Input index="2" value="1"/>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=4 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="4"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="1"/>
<LookupListIndex value="3"/>
</SubstLookupRecord>
<SubstLookupRecord index="2">
<SequenceIndex value="2"/>
<LookupListIndex value="3"/>
</SubstLookupRecord>
<SubstLookupRecord index="3">
<SequenceIndex value="3"/>
<LookupListIndex value="2"/>
</SubstLookupRecord>
</ChainSubClassRule>
<ChainSubClassRule index="2">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=3 -->
<Input index="0" value="1"/>
<Input index="1" value="1"/>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=3 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="4"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="1"/>
<LookupListIndex value="3"/>
</SubstLookupRecord>
<SubstLookupRecord index="2">
<SequenceIndex value="2"/>
<LookupListIndex value="2"/>
</SubstLookupRecord>
</ChainSubClassRule>
<ChainSubClassRule index="3">
<!-- BacktrackGlyphCount=0 -->
<!-- InputGlyphCount=2 -->
<Input index="0" value="1"/>
<!-- LookAheadGlyphCount=0 -->
<!-- SubstCount=2 -->
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="4"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="1"/>
<LookupListIndex value="2"/>
</SubstLookupRecord>
</ChainSubClassRule>
</ChainSubClassSet>
</ChainContextSubst>
</Lookup>
<Lookup index="1">
<LookupType value="5"/>
<LookupFlag value="0"/>
<!-- SubTableCount=2 -->
<ContextSubst index="0" Format="3">
<!-- GlyphCount=3 -->
<!-- SubstCount=2 -->
<Coverage index="0" Format="1">
<Glyph value="glyph00002"/>
</Coverage>
<Coverage index="1" Format="1">
<Glyph value="glyph00004"/>
</Coverage>
<Coverage index="2" Format="1">
<Glyph value="glyph00005"/>
</Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="5"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="2"/>
<LookupListIndex value="5"/>
</SubstLookupRecord>
</ContextSubst>
<ContextSubst index="1" Format="3">
<!-- GlyphCount=2 -->
<!-- SubstCount=2 -->
<Coverage index="0" Format="1">
<Glyph value="glyph00002"/>
</Coverage>
<Coverage index="1" Format="1">
<Glyph value="glyph00005"/>
</Coverage>
<SubstLookupRecord index="0">
<SequenceIndex value="0"/>
<LookupListIndex value="5"/>
</SubstLookupRecord>
<SubstLookupRecord index="1">
<SequenceIndex value="1"/>
<LookupListIndex value="5"/>
</SubstLookupRecord>
</ContextSubst>
</Lookup>
<Lookup index="2">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0" Format="1">
<Substitution in="plus" out="glyph00005"/>
</SingleSubst>
</Lookup>
<Lookup index="3">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0" Format="1">
<Substitution in="plus" out="glyph00004"/>
</SingleSubst>
</Lookup>
<Lookup index="4">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0" Format="1">
<Substitution in="plus" out="glyph00002"/>
</SingleSubst>
</Lookup>
<Lookup index="5">
<LookupType value="1"/>
<LookupFlag value="0"/>
<!-- SubTableCount=1 -->
<SingleSubst index="0" Format="2">
<Substitution in="glyph00002" out="glyph00003"/>
<Substitution in="glyph00005" out="glyph00006"/>
<Substitution in="plus" out="glyph00007"/>
</SingleSubst>
</Lookup>
</LookupList>
</GSUB>
</ttFont>

View File

@ -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():

View File

@ -81,15 +81,6 @@ GVAR_VARIATIONS = {
GVAR_XML = [
'<version value="1"/>',
'<reserved value="0"/>',
'<glyphVariations glyph="space">',
' <tuple>',
' <coord axis="wdth" value="0.7"/>',
' <delta pt="0" x="1" y="11"/>',
' <delta pt="1" x="2" y="22"/>',
' <delta pt="2" x="3" y="33"/>',
' <delta pt="3" x="4" y="44"/>',
' </tuple>',
'</glyphVariations>',
'<glyphVariations glyph="I">',
' <tuple>',
' <coord axis="wght" min="0.0" value="0.5" max="1.0"/>',
@ -113,6 +104,15 @@ GVAR_XML = [
' <delta pt="7" x="1" y="11"/>',
' </tuple>',
'</glyphVariations>',
'<glyphVariations glyph="space">',
' <tuple>',
' <coord axis="wdth" value="0.7"/>',
' <delta pt="0" x="1" y="11"/>',
' <delta pt="1" x="2" y="22"/>',
' <delta pt="2" x="3" y="33"/>',
' <delta pt="3" x="4" y="44"/>',
' </tuple>',
'</glyphVariations>',
]

View File

@ -58,6 +58,19 @@ class MetaTableTest(unittest.TestCase):
'</hexdata>'
], [line.strip() for line in xml.splitlines()][1:])
def test_toXML_ascii_data(self):
table = table__m_e_t_a()
table.data["TEST"] = b"Hello!"
writer = XMLWriter(BytesIO())
table.toXML(writer, {"meta": table})
xml = writer.file.getvalue().decode("utf-8")
self.assertEqual([
'<hexdata tag="TEST">',
'<!-- ascii: Hello! -->',
'48656c6c 6f21',
'</hexdata>'
], [line.strip() for line in xml.splitlines()][1:])
def test_fromXML(self):
table = table__m_e_t_a()
for name, attrs, content in parseXML(

View File

@ -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).

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -572,6 +572,19 @@
<delta pt="21" x="0" y="0"/>
</tuple>
</glyphVariations>
<glyphVariations glyph="dotabovecomb">
<tuple>
<coord axis="wght" value="1.0"/>
<delta pt="0" x="-8" y="28"/>
<delta pt="1" x="13" y="16"/>
<delta pt="2" x="17" y="-13"/>
<delta pt="3" x="-27" y="-20"/>
<delta pt="4" x="0" y="0"/>
<delta pt="5" x="0" y="0"/>
<delta pt="6" x="0" y="0"/>
<delta pt="7" x="0" y="0"/>
</tuple>
</glyphVariations>
<glyphVariations glyph="e">
<tuple>
<coord axis="wght" min="0.0" value="0.36365" max="1.0"/>
@ -614,6 +627,12 @@
<delta pt="16" x="0" y="0"/>
</tuple>
</glyphVariations>
<glyphVariations glyph="edotabove">
<tuple>
<coord axis="wght" value="1.0"/>
<delta pt="1" x="-6" y="91"/>
</tuple>
</glyphVariations>
<glyphVariations glyph="s">
<tuple>
<coord axis="wght" value="1.0"/>
@ -635,25 +654,6 @@
<delta pt="15" x="0" y="0"/>
</tuple>
</glyphVariations>
<glyphVariations glyph="dotabovecomb">
<tuple>
<coord axis="wght" value="1.0"/>
<delta pt="0" x="-8" y="28"/>
<delta pt="1" x="13" y="16"/>
<delta pt="2" x="17" y="-13"/>
<delta pt="3" x="-27" y="-20"/>
<delta pt="4" x="0" y="0"/>
<delta pt="5" x="0" y="0"/>
<delta pt="6" x="0" y="0"/>
<delta pt="7" x="0" y="0"/>
</tuple>
</glyphVariations>
<glyphVariations glyph="edotabove">
<tuple>
<coord axis="wght" value="1.0"/>
<delta pt="1" x="-6" y="91"/>
</tuple>
</glyphVariations>
</gvar>
</ttFont>