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