diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc4733ae5b..ba0a193ee9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: # Oldest supported, 'Stable' supported, Newest supported - python-version: ["3.8", "3.12", "3.13"] + python-version: ["3.9", "3.12", "3.13"] platform: [ubuntu-latest] include: # Only test on the latest supported stable Python on macOS and Windows. - platform: macos-latest diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d0fc3c553e..3f018e26b5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,16 +22,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - pip install setuptools wheel twine + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Build source distribution and pure-python wheel run: | - python setup.py sdist bdist_wheel + uv build - uses: actions/upload-artifact@v4 with: name: pure diff --git a/Doc/README.md b/Doc/README.md index 6927bd62fa..5650be907a 100644 --- a/Doc/README.md +++ b/Doc/README.md @@ -12,32 +12,15 @@ The documentation is hosted at https://fonttools.readthedocs.io/. ## How to Build Local Documentation -### Install Dependencies +You must have a Python 3 interpreter and the `tox` command runner installed on your system to build the fontTools documentation. -You must have a Python 3 interpreter and the `pip` Python package manager installed on your system to build the fontTools documentation. - -Pull the fontTools project source files, create a Python virtual environment, and then install fontTools and the documentation build dependencies by executing the following commands in the root of the fontTools source repository: - -``` -$ pip install -e .[all] -$ pip install -r Doc/docs-requirements.txt -``` - -### Build Documentation - -**With `make`**: execute the following command in the root of the repository: - -``` -$ make docs -``` - -**Without `make`**: execute the following command in the **`Doc` directory**: +Execute the following command in the root of the repository: ``` -$ sphinx-build -b html source build +$ tox -e docs ``` -Open the `Doc/build/html/index.html` file in your browser to view the documentation home page. +Open the `Doc/build/index.html` file in your browser to view the documentation home page. ## Contributing to the Documentation diff --git a/Doc/source/designspaceLib/xml.rst b/Doc/source/designspaceLib/xml.rst index d8c76be99c..f5645b8ca4 100644 --- a/Doc/source/designspaceLib/xml.rst +++ b/Doc/source/designspaceLib/xml.rst @@ -918,12 +918,12 @@ The ```` element contains one or more ```` elements. .. rubric:: Attributes -- ``familyname``: required, string. The family name of the instance - font. Corresponds with ``font.info.familyName`` -- ``stylename``: required, string. The style name of the instance font. - Corresponds with ``font.info.styleName`` -- ``name``: required, string. A unique name that can be used to - identify this font if it needs to be referenced elsewhere. +- ``familyname``: string. Optional if the default source has it set. The family + name of the instance font. Corresponds with ``font.info.familyName`` +- ``stylename``: string. Optional if all axes are fully labled. The style name + of the instance font. Corresponds with ``font.info.styleName`` +- ``name``: string. Optional if all axes are fully labled. A unique name that + can be used to identify this font if it needs to be referenced elsewhere. - ``filename``: string. Required for MutatorMath. A path to the instance file, relative to the root path of this document. The path can be at the same level as the document or lower. diff --git a/Doc/source/encodings/index.rst b/Doc/source/encodings/index.rst index f35e3806eb..97f73d38e9 100644 --- a/Doc/source/encodings/index.rst +++ b/Doc/source/encodings/index.rst @@ -2,20 +2,64 @@ encodings: Support for OpenType-specific character encodings ############################################################ +.. currentmodule:: fontTools.encodings + +.. contents:: On this page: + :local: + + +.. rubric:: Overview: + :heading-level: 2 + 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: +standard Python :mod:`encodings` library, but others still remain unimplemented. +Importing :mod:`fontTools.encodings.codecs` will therefore add string :func:`encode` +and :func:`decode` support for the following encodings: * ``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:: +fontTools also includes a module (:mod:`fontTools.encodings.MacRoman`) that +consists of a mapping of glyph IDs to glyph names in the MacRoman character set:: >>> from fontTools.encodings.MacRoman import MacRoman >>> MacRoman[26] 'twosuperior' + +and a module (:mod:`fontTools.encodings.StandardEncoding`) that provides +a similar mapping of glyph IDs to glyph names in the PostScript Standard +Encoding. + + + +fontTools.encodings.codecs +-------------------------- + +.. currentmodule:: fontTools.encodings.codecs + +.. automodule:: fontTools.encodings.codecs + :members: + :undoc-members: + + +fontTools.encodings.MacRoman +---------------------------- + +.. currentmodule:: fontTools.encodings.MacRoman + +.. automodule:: fontTools.encodings.MacRoman + :members: + :undoc-members: + +fontTools.encodings.StandardEncoding +------------------------------------ + +.. currentmodule:: fontTools.encodings.StandardEncoding + +.. automodule:: fontTools.encodings.StandardEncoding + :members: + :undoc-members: + diff --git a/Doc/source/feaLib/ast.rst b/Doc/source/feaLib/ast.rst new file mode 100644 index 0000000000..b862d42698 --- /dev/null +++ b/Doc/source/feaLib/ast.rst @@ -0,0 +1,37 @@ +#################################################### +ast: Interrogate and generate OpenType feature files +#################################################### + +.. currentmodule:: fontTools.feaLib.astt + +.. rubric:: Overview: + :heading-level: 2 + +feaLib's :mod:`.ast` module provides classes that represent the objects +and structures used to define OpenType feature lookups in the ``fea`` +syntax. After your code has parsed a ``.fea`` file into an abstract +syntax tree (AST), you can use these classes to modify existing +lookups or create new lookups and features. + +The root of the AST representation of a parsed ``.fea`` file is a +:class:`fontTools.feaLib.ast.FeatureFile` object. You can walk the +tree by examining its :attr:`statements` attribute. Nodes in the +tree have an :meth:`asFea` method that will return a ``.fea`` +formated string representation, including correct indentation of block elements. + + +.. _`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 + :members: + :undoc-members: + + .. rubric:: Module members + :heading-level: 2 + + + diff --git a/Doc/source/feaLib/index.rst b/Doc/source/feaLib/index.rst index c25532caa3..a0236ab431 100644 --- a/Doc/source/feaLib/index.rst +++ b/Doc/source/feaLib/index.rst @@ -2,10 +2,36 @@ feaLib: Read and write OpenType feature files ############################################# -fontTools' ``feaLib`` allows for the creation and parsing of Adobe +.. currentmodule:: fontTools.feaLib + +.. contents:: On this page: + :local: + + +.. rubric:: Overview + :heading-level: 2 + +fontTools' :mod:`.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 `_. +``.fea`` files are primarily used for writing human-readable +definitions for the OpenType features stored in a font's ``GSUB`` and +``GPOS`` tables. + +Supporting modules +------------------ + +feaLib contains modules for parsing and inspecting ``.fea`` files +as well as utilities for converting ``.fea`` rules into ``GSUB`` and +``GPOS`` tables and inserting them into fonts. + + .. toctree:: + :maxdepth: 1 + + parser + ast + 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 @@ -13,28 +39,33 @@ 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. -Parsing -------- -.. autoclass:: fontTools.feaLib.parser.Parser - :members: parse - :member-order: bysource -Building ---------- +fontTools.feaLib.builder +------------------------ + +.. currentmodule:: fontTools.feaLib.builder .. automodule:: fontTools.feaLib.builder - :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString + :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString, Builder + :undoc-members: + + +fontTools.feaLib.lookupDebugInfo +-------------------------------- + +.. currentmodule:: fontTools.feaLib.lookupDebugInfo + +.. automodule:: fontTools.feaLib.lookupDebugInfo + :members: + :undoc-members: -Generation/Interrogation ------------------------- -.. _`glyph-containing object`: -.. _`glyph-containing objects`: +fontTools.feaLib.error +---------------------- -In the below, a **glyph-containing object** is an object of one of the following -classes: :class:`GlyphName`, :class:`GlyphClass`, :class:`GlyphClassName`. +.. currentmodule:: fontTools.feaLib.error -.. automodule:: fontTools.feaLib.ast - :member-order: bysource +.. automodule:: fontTools.feaLib.error :members: + :undoc-members: diff --git a/Doc/source/feaLib/parser.rst b/Doc/source/feaLib/parser.rst new file mode 100644 index 0000000000..62c492c2f1 --- /dev/null +++ b/Doc/source/feaLib/parser.rst @@ -0,0 +1,62 @@ +################################################# +parser: Lexing and parsing OpenType feature files +################################################# + +.. currentmodule:: fontTools.feaLib + +.. contents:: On this page: + :local: + + +.. rubric:: Overview + :heading-level: 2 + + +The primary interface for processing ``.fea`` files is +:class:`fontTools.feaLib.parser.Parser`. At a lower level, the +:mod:`fontTools.feaLib.lexer` module implements feaLib's lexical +analysis of the ``.fea`` language syntax, augmented by several smaller +utility modules. + + +Parsing +------- + +.. currentmodule:: fontTools.feaLib.parser + +.. automodule:: fontTools.feaLib.parser + :members: + :undoc-members: + + +Lexing +------ + +.. currentmodule:: fontTools.feaLib.lexer + +.. automodule:: fontTools.feaLib.lexer + :members: + :undoc-members: + + + +fontTools.feaLib.variableScalar +------------------------------- + +.. currentmodule:: fontTools.feaLib.variableScalar + +.. automodule:: fontTools.feaLib.variableScalar + :members: + :undoc-members: + + + +fontTools.feaLib.location +------------------------- + +.. currentmodule:: fontTools.feaLib.location + +.. automodule:: fontTools.feaLib.location + :members: + :undoc-members: + diff --git a/Doc/source/index.rst b/Doc/source/index.rst index e98d902a23..e191a33e80 100644 --- a/Doc/source/index.rst +++ b/Doc/source/index.rst @@ -19,7 +19,7 @@ Installation .. note:: - fontTools requires `Python `_ 3.8 or later. + fontTools requires `Python `_ 3.9 or later. To install fontTools, use `pip `_: diff --git a/Doc/source/misc/arrayTools.rst b/Doc/source/misc/arrayTools.rst index d996cc2030..00ed11772d 100644 --- a/Doc/source/misc/arrayTools.rst +++ b/Doc/source/misc/arrayTools.rst @@ -2,8 +2,9 @@ arrayTools: Various array and rectangle tools ############################################# +.. currentmodule:: fontTools.misc.arrayTools + .. automodule:: fontTools.misc.arrayTools - :member-order: bysource - :inherited-members: :members: :undoc-members: + :member-order: bysource diff --git a/Doc/source/misc/bezierTools.rst b/Doc/source/misc/bezierTools.rst index 10ddc3aa5d..5103555f7c 100644 --- a/Doc/source/misc/bezierTools.rst +++ b/Doc/source/misc/bezierTools.rst @@ -2,7 +2,8 @@ bezierTools: Routines for working with Bezier curves #################################################### +.. currentmodule:: fontTools.misc.bezierTools + .. automodule:: fontTools.misc.bezierTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/classifyTools.rst b/Doc/source/misc/classifyTools.rst index 38c35d4c45..d18127d159 100644 --- a/Doc/source/misc/classifyTools.rst +++ b/Doc/source/misc/classifyTools.rst @@ -1,8 +1,9 @@ -############# -classifyTools -############# +########################################### +classifyTools: Tools for set classification +########################################### + +.. currentmodule:: fontTools.misc.classifyTools .. automodule:: fontTools.misc.classifyTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/cliTools.rst b/Doc/source/misc/cliTools.rst index 36b2aeb9c3..01a50cc721 100644 --- a/Doc/source/misc/cliTools.rst +++ b/Doc/source/misc/cliTools.rst @@ -2,7 +2,8 @@ cliTools: Utilities for command-line interfaces and console scripts ################################################################### +.. currentmodule:: fontTools.misc.cliTools + .. automodule:: fontTools.misc.cliTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/configTools.rst b/Doc/source/misc/configTools.rst index 1d93541197..7e29242a13 100644 --- a/Doc/source/misc/configTools.rst +++ b/Doc/source/misc/configTools.rst @@ -1,8 +1,9 @@ -########### -configTools -########### +############################################################ +configTools: Tools for interfacing with Python configuration +############################################################ + +.. currentmodule:: fontTools.misc.configTools .. automodule:: fontTools.misc.configTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/eexec.rst b/Doc/source/misc/eexec.rst index b229d58540..b0b5537183 100644 --- a/Doc/source/misc/eexec.rst +++ b/Doc/source/misc/eexec.rst @@ -1,8 +1,9 @@ -############################################################### -eexec: PostScript charstring encryption and decryption routines -############################################################### +################################################################### +eexec: Routines for PostScript CharString encryption and decryption +################################################################### + +.. currentmodule:: fontTools.misc.eexec .. automodule:: fontTools.misc.eexec - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/encodingTools.rst b/Doc/source/misc/encodingTools.rst index 4e4b71975c..1baceae710 100644 --- a/Doc/source/misc/encodingTools.rst +++ b/Doc/source/misc/encodingTools.rst @@ -1,8 +1,9 @@ -############# -encodingTools -############# +########################################### +encodingTools: Tools for OpenType encodings +########################################### + +.. currentmodule:: fontTools.misc.encodingTools .. automodule:: fontTools.misc.encodingTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/etree.rst b/Doc/source/misc/etree.rst index 4679a1dd4c..5f67b98540 100644 --- a/Doc/source/misc/etree.rst +++ b/Doc/source/misc/etree.rst @@ -1,8 +1,13 @@ -##### -etree -##### +############################################## +etree: Tools for accessing the ElementTree API +############################################## + +.. currentmodule:: fontTools.misc.etree + +Note also that :mod:`etree` supports some :doc:`optional ` +external libraries. + .. automodule:: fontTools.misc.etree - :inherited-members: :members: - :undoc-members: \ No newline at end of file + :undoc-members: diff --git a/Doc/source/misc/filenames.rst b/Doc/source/misc/filenames.rst index 2ebef3535b..4ecd406049 100644 --- a/Doc/source/misc/filenames.rst +++ b/Doc/source/misc/filenames.rst @@ -1,6 +1,8 @@ -########################################################## -filenames: Implements UFO User Name to File Name Algorithm -########################################################## +################################################################### +filenames: Implementation of UFO's User-Name-to-File-Name algorithm +################################################################### + +.. currentmodule:: fontTools.misc.filenames .. automodule:: fontTools.misc.filenames :members: userNameToFileName diff --git a/Doc/source/misc/fixedTools.rst b/Doc/source/misc/fixedTools.rst index d3785f43f1..f6ea26167c 100644 --- a/Doc/source/misc/fixedTools.rst +++ b/Doc/source/misc/fixedTools.rst @@ -2,7 +2,8 @@ fixedTools: Tools for working with fixed-point numbers ###################################################### +.. currentmodule:: fontTools.misc.fixedTools + .. automodule:: fontTools.misc.fixedTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/index.rst b/Doc/source/misc/index.rst index cfe5205578..2ce315204b 100644 --- a/Doc/source/misc/index.rst +++ b/Doc/source/misc/index.rst @@ -2,11 +2,17 @@ misc: Miscellaneous libraries helpful for font engineering ########################################################## -This is a collection of packages, most of which are used as internal support -utilities by fontTools, but some of which may be more generally useful. +.. currentmodule:: fontTools.misc + +.. rubric:: Overview: + :heading-level: 2 + +:mod:`fontTools.misc` is a collection of modules, most of which are +used as internal support utilities by fontTools, but some of which may +be more generally useful. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 arrayTools bezierTools diff --git a/Doc/source/misc/intTools.rst b/Doc/source/misc/intTools.rst index 24ea231f47..33cc461aa5 100644 --- a/Doc/source/misc/intTools.rst +++ b/Doc/source/misc/intTools.rst @@ -2,5 +2,8 @@ intTools: Tools for working with integer values ############################################### +.. currentmodule:: fontTools.misc.intTools + .. automodule:: fontTools.misc.intTools :members: + :undoc-members: diff --git a/Doc/source/misc/loggingTools.rst b/Doc/source/misc/loggingTools.rst index 157e02095e..03cc398b41 100644 --- a/Doc/source/misc/loggingTools.rst +++ b/Doc/source/misc/loggingTools.rst @@ -1,7 +1,9 @@ ################################################################### -loggingTools: tools for interfacing with the Python logging package +loggingTools: Tools for interfacing with the Python logging package ################################################################### +.. currentmodule:: fontTools.misc.loggingTools + .. automodule:: fontTools.misc.loggingTools :members: :undoc-members: diff --git a/Doc/source/misc/macCreatorType.rst b/Doc/source/misc/macCreatorType.rst index 809098dc7d..93088208c0 100644 --- a/Doc/source/misc/macCreatorType.rst +++ b/Doc/source/misc/macCreatorType.rst @@ -2,8 +2,14 @@ macCreatorType: Functions for working with Mac file attributes ############################################################## -This module requires the `xattr `_ module -to be installed in order to function correctly. +.. currentmodule:: fontTools.misc.macCreatorType + +.. rubric:: Overview: + :heading-level: 2 + +Note: this module requires the `xattr `_ +module to be installed in order to function correctly. .. automodule:: fontTools.misc.macCreatorType :members: + :undoc-members: diff --git a/Doc/source/misc/macRes.rst b/Doc/source/misc/macRes.rst index 6fce8e2e2c..c9e5c36acb 100644 --- a/Doc/source/misc/macRes.rst +++ b/Doc/source/misc/macRes.rst @@ -2,10 +2,23 @@ macRes: Tools for reading Mac resource forks ############################################ -Classic Mac OS files are made up of two parts - the "data fork" which contains the file contents proper, and the "resource fork" which contains a number of structured data items called "resources". Some fonts, such as Mac "font suitcases" and Type 1 LWFN fonts, still use the resource fork for this kind of structured data, and so to read them, fontTools needs to have access to resource forks. +.. currentmodule:: fontTools.misc.macRes -The Inside Macintosh volume `More Macintosh Toolbox `_ explains the structure of resource and data forks. +.. rubric:: Overview: + :heading-level: 2 + +Classic Mac OS files are made up of two parts - the "data fork" which +contains the file contents proper, and the "resource fork" which +contains a number of structured data items called "resources". Some +fonts, such as Mac "font suitcases" and Type 1 LWFN fonts, still use +the resource fork for this kind of structured data, and so to read +them, fontTools needs to have access to resource forks. + +The Inside Macintosh volume `More Macintosh Toolbox +`_ +explains the structure of resource and data forks. .. automodule:: fontTools.misc.macRes :members: ResourceReader, Resource - :member-order: bysource \ No newline at end of file + :undoc-members: + :member-order: bysource diff --git a/Doc/source/misc/plistlib.rst b/Doc/source/misc/plistlib.rst index 7409aa222a..7f78e03505 100644 --- a/Doc/source/misc/plistlib.rst +++ b/Doc/source/misc/plistlib.rst @@ -2,5 +2,8 @@ plistlib: Tools for handling .plist files ######################################### +.. currentmodule:: fontTools.misc.plistlib + .. automodule:: fontTools.misc.plistlib :members: totree, fromtree, load, loads, dump, dumps + :undoc-members: diff --git a/Doc/source/misc/psCharStrings.rst b/Doc/source/misc/psCharStrings.rst index 58497f65b6..34a9a8ccb1 100644 --- a/Doc/source/misc/psCharStrings.rst +++ b/Doc/source/misc/psCharStrings.rst @@ -1,8 +1,9 @@ -############# -psCharStrings -############# +##################################################### +psCharStrings: Tools for working with CharString data +##################################################### + +.. currentmodule:: fontTools.misc.psCharStrings .. automodule:: fontTools.misc.psCharStrings - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/psLib.rst b/Doc/source/misc/psLib.rst index f3afa8bf70..b0a100fc20 100644 --- a/Doc/source/misc/psLib.rst +++ b/Doc/source/misc/psLib.rst @@ -1,8 +1,9 @@ -##### -psLib -##### +############################################# +psLib: Tools for working with PostScript data +############################################# + +.. currentmodule:: fontTools.misc.psLib .. automodule:: fontTools.misc.psLib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/psOperators.rst b/Doc/source/misc/psOperators.rst index 432274e6df..966e1ae8ef 100644 --- a/Doc/source/misc/psOperators.rst +++ b/Doc/source/misc/psOperators.rst @@ -1,8 +1,9 @@ -########### -psOperators -########### +######################################################## +psOperators: Tools for working with PostScript operators +######################################################## + +.. currentmodule:: fontTools.misc.psOperators .. automodule:: fontTools.misc.psOperators - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/sstruct.rst b/Doc/source/misc/sstruct.rst index 0544795fcf..9e14907a06 100644 --- a/Doc/source/misc/sstruct.rst +++ b/Doc/source/misc/sstruct.rst @@ -1,8 +1,9 @@ -####### -sstruct -####### +################################################## +sstruct: Tools for working with Python struct data +################################################## + +.. currentmodule:: fontTools.misc.sstruct .. automodule:: fontTools.misc.sstruct - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/symfont.rst b/Doc/source/misc/symfont.rst index c189f3d9d9..2eb8159338 100644 --- a/Doc/source/misc/symfont.rst +++ b/Doc/source/misc/symfont.rst @@ -1,8 +1,12 @@ -####### -symfont -####### +#################################################################### +symfont: Tools for working with Beziers through symbolic mathematics +#################################################################### +.. currentmodule:: fontTools.misc.symfont + +Note also that :mod:`misc.symfont` supports some :doc:`optional ` +external libraries. + .. automodule:: fontTools.misc.symfont - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/testTools.rst b/Doc/source/misc/testTools.rst index 00197656c8..8baad6367c 100644 --- a/Doc/source/misc/testTools.rst +++ b/Doc/source/misc/testTools.rst @@ -1,8 +1,9 @@ -######### -testTools -######### +################################# +testTools: Tools for unit testing +################################# + +.. currentmodule:: fontTools.misc.testTools .. automodule:: fontTools.misc.testTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/textTools.rst b/Doc/source/misc/textTools.rst index 0044c082c8..50b11d1528 100644 --- a/Doc/source/misc/textTools.rst +++ b/Doc/source/misc/textTools.rst @@ -1,8 +1,9 @@ -######### -textTools -######### +########################################### +textTools: Tools for working with text data +########################################### + +.. currentmodule:: fontTools.misc.textTools .. automodule:: fontTools.misc.textTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/timeTools.rst b/Doc/source/misc/timeTools.rst index c0a7199062..256a2e5928 100644 --- a/Doc/source/misc/timeTools.rst +++ b/Doc/source/misc/timeTools.rst @@ -1,8 +1,9 @@ -######### -timeTools -######### +##################################################### +timeTools: Tools for working with OpenType timestamps +##################################################### + +.. currentmodule:: fontTools.misc.timeTools .. automodule:: fontTools.misc.timeTools - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/transform.rst b/Doc/source/misc/transform.rst index 44a3dbdfef..7f8cb47636 100644 --- a/Doc/source/misc/transform.rst +++ b/Doc/source/misc/transform.rst @@ -1,8 +1,9 @@ -######### -transform -######### +######################################################### +transform: Tools for working with transformation matrices +######################################################### + +.. currentmodule:: fontTools.misc.transform .. automodule:: fontTools.misc.transform - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/xmlReader.rst b/Doc/source/misc/xmlReader.rst index d0b80f5ba5..fb276165e1 100644 --- a/Doc/source/misc/xmlReader.rst +++ b/Doc/source/misc/xmlReader.rst @@ -1,8 +1,9 @@ -######### -xmlReader -######### +##################################### +xmlReader: Tools for reading XML data +##################################### + +.. currentmodule:: fontTools.misc.xmlReader .. automodule:: fontTools.misc.xmlReader - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/misc/xmlWriter.rst b/Doc/source/misc/xmlWriter.rst index 5f7aaef5e4..22550ee589 100644 --- a/Doc/source/misc/xmlWriter.rst +++ b/Doc/source/misc/xmlWriter.rst @@ -1,8 +1,9 @@ -######### -xmlWriter -######### +##################################### +xmlWriter: Tools for writing XML data +##################################### + +.. currentmodule:: fontTools.misc.xmlWriter .. automodule:: fontTools.misc.xmlWriter - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/optional.rst b/Doc/source/optional.rst index 0ac22bfd97..b9261c986b 100644 --- a/Doc/source/optional.rst +++ b/Doc/source/optional.rst @@ -36,8 +36,6 @@ Package for reading and writing UFO source files; it requires: * `fs `__: (aka ``pyfilesystem2``) filesystem abstraction layer. -* `enum34 `__: backport for the built-in ``enum`` module (only required on Python < 3.4). - *Extra:* ``ufo`` diff --git a/Doc/source/otlLib/index.rst b/Doc/source/otlLib/index.rst index b1502b0537..bc6efa9053 100644 --- a/Doc/source/otlLib/index.rst +++ b/Doc/source/otlLib/index.rst @@ -2,7 +2,13 @@ otlLib: Routines for working with OpenType Layout ################################################# -The ``fontTools.otlLib`` library provides routines to help you create the +.. currentmodule:: fontTools.otlLib + +.. contents:: On this page: + :local: + + +The :mod:`fontTools.otlLib` library provides routines to help you create the subtables and other data structures you need when you are editing a font's ``GSUB`` and ``GPOS`` tables: substitution and positioning rules, anchors, lookups, coverage tables and so on. diff --git a/Doc/source/pens/areaPen.rst b/Doc/source/pens/areaPen.rst index f3f21bbbf0..03a751be5e 100644 --- a/Doc/source/pens/areaPen.rst +++ b/Doc/source/pens/areaPen.rst @@ -3,6 +3,5 @@ areaPen ####### .. automodule:: fontTools.pens.areaPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/basePen.rst b/Doc/source/pens/basePen.rst index 87bf832b37..00e74df482 100644 --- a/Doc/source/pens/basePen.rst +++ b/Doc/source/pens/basePen.rst @@ -1,8 +1,7 @@ -####### -basePen -####### +############################################### +basePen: Base classes for segment-oriented pens +############################################### .. automodule:: fontTools.pens.basePen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/boundsPen.rst b/Doc/source/pens/boundsPen.rst index a0d9ab41ae..8de5620150 100644 --- a/Doc/source/pens/boundsPen.rst +++ b/Doc/source/pens/boundsPen.rst @@ -3,6 +3,5 @@ boundsPen ######### .. automodule:: fontTools.pens.boundsPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/cairoPen.rst b/Doc/source/pens/cairoPen.rst new file mode 100644 index 0000000000..eb3b0afb5f --- /dev/null +++ b/Doc/source/pens/cairoPen.rst @@ -0,0 +1,7 @@ +######## +cairoPen +######## + +.. automodule:: fontTools.pens.cairoPen + :members: + :undoc-members: diff --git a/Doc/source/pens/cocoaPen.rst b/Doc/source/pens/cocoaPen.rst index bbe8050ee1..32c8e7886b 100644 --- a/Doc/source/pens/cocoaPen.rst +++ b/Doc/source/pens/cocoaPen.rst @@ -2,7 +2,9 @@ cocoaPen ######## + Note also that :mod:`cocoaPen` supports some :doc:`optional ` + external libraries. + .. automodule:: fontTools.pens.cocoaPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/cu2quPen.rst b/Doc/source/pens/cu2quPen.rst index 4ae0249eba..ba9136903d 100644 --- a/Doc/source/pens/cu2quPen.rst +++ b/Doc/source/pens/cu2quPen.rst @@ -3,6 +3,5 @@ cu2quPen ######## .. automodule:: fontTools.pens.cu2quPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/filterPen.rst b/Doc/source/pens/filterPen.rst index c79b944cf1..0b484a4c1a 100644 --- a/Doc/source/pens/filterPen.rst +++ b/Doc/source/pens/filterPen.rst @@ -3,6 +3,5 @@ filterPen ######### .. automodule:: fontTools.pens.filterPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/freetypePen.rst b/Doc/source/pens/freetypePen.rst index 9b849a214f..3b2e2a095d 100644 --- a/Doc/source/pens/freetypePen.rst +++ b/Doc/source/pens/freetypePen.rst @@ -3,6 +3,5 @@ freetypePen ########### .. automodule:: fontTools.pens.freetypePen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/index.rst b/Doc/source/pens/index.rst index e26d6d5b20..bad859de07 100644 --- a/Doc/source/pens/index.rst +++ b/Doc/source/pens/index.rst @@ -2,8 +2,9 @@ pens: Inspect and manipulate glyph outlines ########################################### + .. rubric:: Overview: - :heading-level: 3 + :heading-level: 2 The fontTools **pens** are a collection of classes that can operate on a font glyph via the points and the contours of the glyph's outlines. @@ -13,32 +14,69 @@ such as a new glyph outline or a formatted image, but other pens analyze the outlines and return information about the glyph. Pens that alter or produce a pen-compatible :class:`.ttGlyph` object can -be chained together. +be chained together by calling he :meth:`.draw` method of each glyph +object, in turn, with a subsequent pen argument. + +The majority of the pens process glyph outlines in a segment-oriented +manner, meaning that they operate by processing the Bezier segments of +each glyph or glyph component in sequential order. This model +corresponds to the way that glyph data is stored in both +TrueType-flavored and CFF-flavored OpenType fonts; the documentation +often refers to pens of this type as *SegmentPens*. New pens can be written by sub-classing the :class:`.AbstractPen` or, -somewhat more practically, :class:`.BasePen` classes. The Pen Protocol is -documented on the :ref:`basePen` page. +somewhat more practically, :class:`.BasePen` classes. The general Pen +Protocol is documented on the :doc:`basePen ` page: + +.. toctree:: + :maxdepth: 2 + + basePen + +There are also several "point-oriented" pens in the collection. These +pens serve to interpret the storage format used in Unified Font Object +(UFO) source files, which records all of the points of each contour in +sequential order, rather than as Bezier-curve segments. Thus, these +*PointPens* operate only on sequences of points. + +UFO's point-only file format can be deterministically converted into +segment-oriented form and vice-versa; therefore all pens are available +to be used with UFO sources. General examples can be found on the +:doc:`pointPen ` page: + +.. toctree:: + :maxdepth: 2 + + pointPen + +but there are also ``-PointPen`` variants available for several of the +other pens, included alongside the modules for their segment-oriented +version. + + + +Pen modules +----------- + + +Note: + Some of the platform-specific pen modules rely on importing external + Python libraries; these cases are noted on the relevant pen modules' + pages. + -.. rubric:: Pen modules: - :heading-level: 3 .. toctree:: - :maxdepth: 1 + :maxdepth: 2 areaPen - basePen boundsPen - cocoaPen cu2quPen filterPen - freetypePen momentsPen perimeterPen pointInsidePen - pointPen - qtPen recordingPen - reportLabPen reverseContourPen roundingPen statisticsPen @@ -47,5 +85,15 @@ documented on the :ref:`basePen` page. teePen transformPen ttGlyphPen + +.. toctree:: + :maxdepth: 2 + + cairoPen + cocoaPen + freetypePen + qtPen + quartzPen + reportLabPen wxPen diff --git a/Doc/source/pens/momentsPen.rst b/Doc/source/pens/momentsPen.rst index 4587f75b4e..dbcfcd2baa 100644 --- a/Doc/source/pens/momentsPen.rst +++ b/Doc/source/pens/momentsPen.rst @@ -3,6 +3,5 @@ momentsPen ########## .. automodule:: fontTools.pens.momentsPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/perimeterPen.rst b/Doc/source/pens/perimeterPen.rst index c625a3dc2c..97feccaa12 100644 --- a/Doc/source/pens/perimeterPen.rst +++ b/Doc/source/pens/perimeterPen.rst @@ -3,6 +3,5 @@ perimeterPen ############ .. automodule:: fontTools.pens.perimeterPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/pointInsidePen.rst b/Doc/source/pens/pointInsidePen.rst index 81a4b2e8cb..9954e478ac 100644 --- a/Doc/source/pens/pointInsidePen.rst +++ b/Doc/source/pens/pointInsidePen.rst @@ -3,6 +3,5 @@ pointInsidePen ############## .. automodule:: fontTools.pens.pointInsidePen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/pointPen.rst b/Doc/source/pens/pointPen.rst index 09b9897782..055b995385 100644 --- a/Doc/source/pens/pointPen.rst +++ b/Doc/source/pens/pointPen.rst @@ -1,8 +1,12 @@ -######## -pointPen -######## +#################################### +pointPen: Point-oriented pen classes +#################################### + +.. currentmodule:: fontTools.pens.pointPen + +This module contains base classes for point-oriented :doc:`pens +`. .. automodule:: fontTools.pens.pointPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/qtPen.rst b/Doc/source/pens/qtPen.rst index bfaa9b5062..746f6ad218 100644 --- a/Doc/source/pens/qtPen.rst +++ b/Doc/source/pens/qtPen.rst @@ -2,7 +2,10 @@ qtPen ##### + Note also that :mod:`qtPen` supports some :doc:`optional ` + external libraries. + + .. automodule:: fontTools.pens.qtPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/quartzPen.rst b/Doc/source/pens/quartzPen.rst new file mode 100644 index 0000000000..1b7f75217e --- /dev/null +++ b/Doc/source/pens/quartzPen.rst @@ -0,0 +1,7 @@ +######### +quartzPen +######### + +.. automodule:: fontTools.pens.quartzPen + :members: + :undoc-members: diff --git a/Doc/source/pens/recordingPen.rst b/Doc/source/pens/recordingPen.rst index ee9178c5a1..69e7ceb5fd 100644 --- a/Doc/source/pens/recordingPen.rst +++ b/Doc/source/pens/recordingPen.rst @@ -3,6 +3,5 @@ recordingPen ############ .. automodule:: fontTools.pens.recordingPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/reportLabPen.rst b/Doc/source/pens/reportLabPen.rst index 7fe878475b..8f592cbb5f 100644 --- a/Doc/source/pens/reportLabPen.rst +++ b/Doc/source/pens/reportLabPen.rst @@ -2,7 +2,10 @@ reportLabPen ############ + Note also that :mod:`reportLabPen` supports some :doc:`optional ` + external libraries. + + .. automodule:: fontTools.pens.reportLabPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/reverseContourPen.rst b/Doc/source/pens/reverseContourPen.rst index 8178e2ca23..769dc83aa5 100644 --- a/Doc/source/pens/reverseContourPen.rst +++ b/Doc/source/pens/reverseContourPen.rst @@ -3,6 +3,5 @@ reverseContourPen ################# .. automodule:: fontTools.pens.reverseContourPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/roundingPen.rst b/Doc/source/pens/roundingPen.rst index 7eb4214d03..8db7a57c0c 100644 --- a/Doc/source/pens/roundingPen.rst +++ b/Doc/source/pens/roundingPen.rst @@ -3,6 +3,5 @@ roundingPen ########### .. automodule:: fontTools.pens.roundingPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/statisticsPen.rst b/Doc/source/pens/statisticsPen.rst index e06e3220a3..efa16089a8 100644 --- a/Doc/source/pens/statisticsPen.rst +++ b/Doc/source/pens/statisticsPen.rst @@ -3,6 +3,5 @@ statisticsPen ############# .. automodule:: fontTools.pens.statisticsPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/svgPathPen.rst b/Doc/source/pens/svgPathPen.rst index 45bf15167b..a3a43785b2 100644 --- a/Doc/source/pens/svgPathPen.rst +++ b/Doc/source/pens/svgPathPen.rst @@ -3,6 +3,5 @@ svgPathPen ########## .. automodule:: fontTools.pens.svgPathPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/t2CharStringPen.rst b/Doc/source/pens/t2CharStringPen.rst index 9d55391f98..d58c67aa04 100644 --- a/Doc/source/pens/t2CharStringPen.rst +++ b/Doc/source/pens/t2CharStringPen.rst @@ -3,6 +3,5 @@ t2CharStringPen ############### .. automodule:: fontTools.pens.t2CharStringPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/teePen.rst b/Doc/source/pens/teePen.rst index 2a4558d5e2..7a0313c7ff 100644 --- a/Doc/source/pens/teePen.rst +++ b/Doc/source/pens/teePen.rst @@ -3,6 +3,5 @@ teePen ###### .. automodule:: fontTools.pens.teePen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/transformPen.rst b/Doc/source/pens/transformPen.rst index 3bb802a522..5b414f8429 100644 --- a/Doc/source/pens/transformPen.rst +++ b/Doc/source/pens/transformPen.rst @@ -3,6 +3,5 @@ transformPen ############ .. automodule:: fontTools.pens.transformPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/ttGlyphPen.rst b/Doc/source/pens/ttGlyphPen.rst index e1bf7010f2..df3db36c58 100644 --- a/Doc/source/pens/ttGlyphPen.rst +++ b/Doc/source/pens/ttGlyphPen.rst @@ -3,6 +3,5 @@ ttGlyphPen ########## .. automodule:: fontTools.pens.ttGlyphPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/pens/wxPen.rst b/Doc/source/pens/wxPen.rst index 37ce50acc6..152b92111d 100644 --- a/Doc/source/pens/wxPen.rst +++ b/Doc/source/pens/wxPen.rst @@ -3,6 +3,5 @@ wxPen ##### .. automodule:: fontTools.pens.wxPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/subset/cff.rst b/Doc/source/subset/cff.rst index 8c21c3966e..bf9461dc86 100644 --- a/Doc/source/subset/cff.rst +++ b/Doc/source/subset/cff.rst @@ -1,8 +1,9 @@ -### -cff -### +############################################ +cff: Tools for subsetting CFF-flavored fonts +############################################ + +.. currentmodule:: fontTools.subset.cff .. automodule:: fontTools.subset.cff - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/subset/index.rst b/Doc/source/subset/index.rst index 1bc255b0f3..30067801d0 100644 --- a/Doc/source/subset/index.rst +++ b/Doc/source/subset/index.rst @@ -2,12 +2,13 @@ subset: Generate subsets of fonts or optimize file sizes ######################################################## +.. currentmodule:: fontTools.subset + .. toctree:: :maxdepth: 1 cff .. automodule:: fontTools.subset - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttx.rst b/Doc/source/ttx.rst index 62b91b45d1..a711ef1c05 100644 --- a/Doc/source/ttx.rst +++ b/Doc/source/ttx.rst @@ -36,13 +36,13 @@ The TTX file format The following tables are currently supported:: BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, Debg, 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, VARC, 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 + FFTM, Feat, GDEF, GMAP, GPKG, GPOS, GSUB, GVAR, 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, VARC, 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 diff --git a/Doc/source/ufoLib/converters.rst b/Doc/source/ufoLib/converters.rst index 0202d43358..134be52d09 100644 --- a/Doc/source/ufoLib/converters.rst +++ b/Doc/source/ufoLib/converters.rst @@ -3,6 +3,6 @@ converters: Conversion functions for kerning and groups ####################################################### .. automodule:: fontTools.ufoLib.converters - :inherited-members: + :no-inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/errors.rst b/Doc/source/ufoLib/errors.rst index 179e968ee1..1896ce516d 100644 --- a/Doc/source/ufoLib/errors.rst +++ b/Doc/source/ufoLib/errors.rst @@ -3,6 +3,5 @@ errors: Exceptions for handling UFO-specific errors ################################################### .. automodule:: fontTools.ufoLib.errors - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/etree.rst b/Doc/source/ufoLib/etree.rst new file mode 100644 index 0000000000..bd43f57254 --- /dev/null +++ b/Doc/source/ufoLib/etree.rst @@ -0,0 +1,9 @@ +############################################################# +etree: Shim module for the ElementTree XML API *[deprecated]* +############################################################# + +.. important:: + + .. automodule:: fontTools.ufoLib.etree + :members: + :undoc-members: diff --git a/Doc/source/ufoLib/filenames.rst b/Doc/source/ufoLib/filenames.rst index d3eef04966..41ab4a0c27 100644 --- a/Doc/source/ufoLib/filenames.rst +++ b/Doc/source/ufoLib/filenames.rst @@ -3,6 +3,5 @@ filenames: Functions to convert between file names and user-facing strings ########################################################################## .. automodule:: fontTools.ufoLib.filenames - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/glifLib.rst b/Doc/source/ufoLib/glifLib.rst index 9213260bc4..4e48e75f1b 100644 --- a/Doc/source/ufoLib/glifLib.rst +++ b/Doc/source/ufoLib/glifLib.rst @@ -3,6 +3,5 @@ glifLib: Read and write UFO .glif files ####################################### .. automodule:: fontTools.ufoLib.glifLib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/index.rst b/Doc/source/ufoLib/index.rst index a865fd2f65..2bc5ab2a0d 100644 --- a/Doc/source/ufoLib/index.rst +++ b/Doc/source/ufoLib/index.rst @@ -3,18 +3,18 @@ ufoLib: Read and write Unified Font Object files ################################################ .. rubric:: Overview: - :heading-level: 3 + :heading-level: 2 .. automodule:: fontTools.ufoLib - :inherited-members: + :no-inherited-members: :members: :undoc-members: :member-order: bysource - .. rubric:: Submodules: - :heading-level: 3 + .. rubric:: Modules: + :heading-level: 2 - ufoLib provides the following submodules: + ufoLib provides the following modules: .. toctree:: :maxdepth: 1 @@ -28,16 +28,20 @@ ufoLib: Read and write Unified Font Object files utils validators - Two deprecated submodules are also currently included: + Three deprecated modules are also currently included: .. toctree:: :maxdepth: 1 - + + etree plistlib pointpen + Note also that :mod:`ufoLib` supports some :doc:`optional ` + external libraries. + .. rubric:: Module members: - :heading-level: 3 + :heading-level: 2 .. autodata:: fontInfoAttributesVersion1 .. autodata:: fontInfoAttributesVersion2 diff --git a/Doc/source/ufoLib/kerning.rst b/Doc/source/ufoLib/kerning.rst index b8f1c38c5d..e16f993822 100644 --- a/Doc/source/ufoLib/kerning.rst +++ b/Doc/source/ufoLib/kerning.rst @@ -3,6 +3,5 @@ kerning: Support for accessing kerning data ########################################### .. automodule:: fontTools.ufoLib.kerning - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/plistlib.rst b/Doc/source/ufoLib/plistlib.rst index 6ae1a0ce1d..eb919dce4d 100644 --- a/Doc/source/ufoLib/plistlib.rst +++ b/Doc/source/ufoLib/plistlib.rst @@ -5,6 +5,5 @@ plistlib: Support for reading and writing .plist files *[deprecated]* .. important:: .. automodule:: fontTools.ufoLib.plistlib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/pointpen.rst b/Doc/source/ufoLib/pointpen.rst index d71a727265..0f11bcce0f 100644 --- a/Doc/source/ufoLib/pointpen.rst +++ b/Doc/source/ufoLib/pointpen.rst @@ -5,6 +5,5 @@ pointPen: A pen for accessing points in a glyph contour *[deprecated]* .. important:: .. automodule:: fontTools.ufoLib.pointPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/utils.rst b/Doc/source/ufoLib/utils.rst index bef7828152..2a63e3812a 100644 --- a/Doc/source/ufoLib/utils.rst +++ b/Doc/source/ufoLib/utils.rst @@ -1,8 +1,7 @@ -##################################### -utils: Miscellaneous helper functions -##################################### +######################################### +utils: Miscellaneous UFO helper functions +######################################### .. automodule:: fontTools.ufoLib.utils - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/validators.rst b/Doc/source/ufoLib/validators.rst index c512e873f8..2288378229 100644 --- a/Doc/source/ufoLib/validators.rst +++ b/Doc/source/ufoLib/validators.rst @@ -3,6 +3,5 @@ validators: Data-validation functions ##################################### .. automodule:: fontTools.ufoLib.validators - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/unicodedata/Blocks.rst b/Doc/source/unicodedata/Blocks.rst index 5d01da7ecd..7650f86725 100644 --- a/Doc/source/unicodedata/Blocks.rst +++ b/Doc/source/unicodedata/Blocks.rst @@ -2,8 +2,9 @@ Blocks ###### +.. currentmodule:: fontTools.unicodedata.Blocks + .. automodule:: fontTools.unicodedata.Blocks - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/unicodedata/OTTags.rst b/Doc/source/unicodedata/OTTags.rst index a436bdc4ba..8e3571ab67 100644 --- a/Doc/source/unicodedata/OTTags.rst +++ b/Doc/source/unicodedata/OTTags.rst @@ -2,8 +2,9 @@ OTTags ###### +.. currentmodule:: fontTools.unicodedata.OTTags + .. automodule:: fontTools.unicodedata.OTTags - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/unicodedata/ScriptExtensions.rst b/Doc/source/unicodedata/ScriptExtensions.rst index dce2bbc49f..47d0c92bec 100644 --- a/Doc/source/unicodedata/ScriptExtensions.rst +++ b/Doc/source/unicodedata/ScriptExtensions.rst @@ -2,8 +2,9 @@ ScriptExtensions ################ +.. currentmodule:: fontTools.unicodedata.ScriptExtensions + .. automodule:: fontTools.unicodedata.ScriptExtensions - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/unicodedata/Scripts.rst b/Doc/source/unicodedata/Scripts.rst index 2ec6e34135..d8cc01e07b 100644 --- a/Doc/source/unicodedata/Scripts.rst +++ b/Doc/source/unicodedata/Scripts.rst @@ -2,8 +2,9 @@ Scripts ####### +.. currentmodule:: fontTools.unicodedata.Scripts + .. automodule:: fontTools.unicodedata.Scripts - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/unicodedata/index.rst b/Doc/source/unicodedata/index.rst index 811d65d776..395dd43b49 100644 --- a/Doc/source/unicodedata/index.rst +++ b/Doc/source/unicodedata/index.rst @@ -1,16 +1,41 @@ -########### -unicodedata -########### +########################################################################### +unicodedata: Interface to character and script data in Unicode and OpenType +########################################################################### -.. toctree:: - :maxdepth: 1 +.. currentmodule:: fontTools.unicodedata - Blocks - OTTags - ScriptExtensions - Scripts +.. contents:: On this page: + :local: + +.. rubric:: Overview: + :heading-level: 2 + +:mod:`fontTools.unicodedata` provides a set of functions for accessing +the Unicode properties of characters and for translating various +Unicode entities or identifiers into other formats, such as converting +Unicode script codes to OpenType script tags and vice versa. + + +Supporting modules: +------------------- + +unicodedata also includes helper modules that provide lower-level +access to Unicode block data, script and script extension data, and +OpenType script tags: + + .. toctree:: + :maxdepth: 1 + + Blocks + OTTags + ScriptExtensions + Scripts + + +fontTools.unicodedata +--------------------- .. automodule:: fontTools.unicodedata - :inherited-members: :members: :undoc-members: + :member-order: bysource diff --git a/Doc/source/varLib/builder.rst b/Doc/source/varLib/builder.rst index 3da3d32c14..909e09a87a 100644 --- a/Doc/source/varLib/builder.rst +++ b/Doc/source/varLib/builder.rst @@ -3,6 +3,5 @@ builder ####### .. automodule:: fontTools.varLib.builder - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/cff.rst b/Doc/source/varLib/cff.rst index 62e11c737f..95f34bf076 100644 --- a/Doc/source/varLib/cff.rst +++ b/Doc/source/varLib/cff.rst @@ -3,6 +3,5 @@ cff ### .. automodule:: fontTools.varLib.cff - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/errors.rst b/Doc/source/varLib/errors.rst index b761854c71..1b449e8d21 100644 --- a/Doc/source/varLib/errors.rst +++ b/Doc/source/varLib/errors.rst @@ -3,6 +3,5 @@ errors ###### .. automodule:: fontTools.varLib.errors - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/featureVars.rst b/Doc/source/varLib/featureVars.rst index da73560f81..b2805fac5f 100644 --- a/Doc/source/varLib/featureVars.rst +++ b/Doc/source/varLib/featureVars.rst @@ -3,6 +3,5 @@ featureVars ########### .. automodule:: fontTools.varLib.featureVars - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/index.rst b/Doc/source/varLib/index.rst index 32f428eb55..f8f5e2eef6 100644 --- a/Doc/source/varLib/index.rst +++ b/Doc/source/varLib/index.rst @@ -2,8 +2,23 @@ varLib: Support for OpenType Variations ####################################### +.. contents:: On this page: + :local: + +.. rubric:: Overview + :heading-level: 2 + +The :py:mod:`fontTools.varLib` package contains a number of classes and routines +for handling, building and interpolating variable font data. These routines +rely on a common set of concepts, many of which are equivalent to concepts +in the OpenType Specification, but some of which are unique to :py:mod:`varLib`. + + +Supporting modules +------------------ + .. toctree:: - :maxdepth: 2 + :maxdepth: 1 builder cff @@ -20,10 +35,6 @@ varLib: Support for OpenType Variations plot varStore -The ``fontTools.varLib`` package contains a number of classes and routines -for handling, building and interpolating variable font data. These routines -rely on a common set of concepts, many of which are equivalent to concepts -in the OpenType Specification, but some of which are unique to ``varLib``. Terminology ----------- @@ -108,7 +119,9 @@ support scalar 0 below its minimum or above its maximum. +Package contents +---------------- + .. automodule:: fontTools.varLib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/instancer.rst b/Doc/source/varLib/instancer.rst index 8776de313c..91f416e681 100644 --- a/Doc/source/varLib/instancer.rst +++ b/Doc/source/varLib/instancer.rst @@ -3,6 +3,5 @@ instancer ######### .. automodule:: fontTools.varLib.instancer - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/interpolatable.rst b/Doc/source/varLib/interpolatable.rst index 1120a982a6..cf8f1b871d 100644 --- a/Doc/source/varLib/interpolatable.rst +++ b/Doc/source/varLib/interpolatable.rst @@ -2,7 +2,9 @@ interpolatable ############## +Note also that :mod:`varLib.interpolatable` supports some :doc:`optional ` +external libraries. + .. automodule:: fontTools.varLib.interpolatable - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/interpolate_layout.rst b/Doc/source/varLib/interpolate_layout.rst index a9655b53b4..752f748b02 100644 --- a/Doc/source/varLib/interpolate_layout.rst +++ b/Doc/source/varLib/interpolate_layout.rst @@ -3,6 +3,5 @@ interpolate_layout ################## .. automodule:: fontTools.varLib.interpolate_layout - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/iup.rst b/Doc/source/varLib/iup.rst index b096788b41..336b231c16 100644 --- a/Doc/source/varLib/iup.rst +++ b/Doc/source/varLib/iup.rst @@ -3,6 +3,5 @@ iup ### .. automodule:: fontTools.varLib.iup - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/merger.rst b/Doc/source/varLib/merger.rst index cf0a5a1c9c..37383aa65e 100644 --- a/Doc/source/varLib/merger.rst +++ b/Doc/source/varLib/merger.rst @@ -3,6 +3,5 @@ merger ###### .. automodule:: fontTools.varLib.merger - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/models.rst b/Doc/source/varLib/models.rst index f59f0b84c3..e6c7fa8acd 100644 --- a/Doc/source/varLib/models.rst +++ b/Doc/source/varLib/models.rst @@ -3,6 +3,5 @@ models ###### .. automodule:: fontTools.varLib.models - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/mutator.rst b/Doc/source/varLib/mutator.rst index fffa80382b..e606ab8679 100644 --- a/Doc/source/varLib/mutator.rst +++ b/Doc/source/varLib/mutator.rst @@ -3,6 +3,5 @@ mutator ####### .. automodule:: fontTools.varLib.mutator - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/mvar.rst b/Doc/source/varLib/mvar.rst index 8c59a310da..ec490d90f6 100644 --- a/Doc/source/varLib/mvar.rst +++ b/Doc/source/varLib/mvar.rst @@ -3,8 +3,7 @@ mvar #### .. automodule:: fontTools.varLib.mvar - :inherited-members: :members: :undoc-members: -.. data:: fontTools.varLib.mvar.MVAR_ENTRIES \ No newline at end of file +.. data:: fontTools.varLib.mvar.MVAR_ENTRIES diff --git a/Doc/source/varLib/plot.rst b/Doc/source/varLib/plot.rst index a722a2d617..77322f793e 100644 --- a/Doc/source/varLib/plot.rst +++ b/Doc/source/varLib/plot.rst @@ -2,7 +2,9 @@ plot #### + Note also that :mod:`varLib.plot` supports some :doc:`optional ` + external libraries. + .. automodule:: fontTools.varLib.plot - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/varStore.rst b/Doc/source/varLib/varStore.rst index cc91101e86..390f32b8a4 100644 --- a/Doc/source/varLib/varStore.rst +++ b/Doc/source/varLib/varStore.rst @@ -3,6 +3,5 @@ varStore ######## .. automodule:: fontTools.varLib.varStore - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/voltLib/index.rst b/Doc/source/voltLib/index.rst index a0ced85de0..aa666c9682 100644 --- a/Doc/source/voltLib/index.rst +++ b/Doc/source/voltLib/index.rst @@ -2,33 +2,77 @@ voltLib: Read and write MS VOLT projects ######################################## +.. currentmodule:: fontTools.voltLib + +.. contents:: On this page: + :local: + +.. rubric:: Overview: + :heading-level: 2 + +:mod:`fontTools.voltLib` provides support for working with the project +files from Microsoft's Visual OpenType Layout Tool (VOLT), a Windows +GUI utility used to add and edit OpenType Layout tables in fonts. + +The primary interface is :mod:`fontTools.voltLib.voltToFea`, which +enables conversion of of VOLT files to the Adobe .fea format: + .. toctree:: :maxdepth: 2 voltToFea + +.. rubric:: Modules + :heading-level: 2 + +voltLib also contains modules that implement lower-level parsing, +lexing, and analysis of VOLT project files. + + +fontTools.voltLib +----------------- + .. automodule:: fontTools.voltLib + :members: + :undoc-members: -ast ---- + +fontTools.voltLib.ast +--------------------- + +.. currentmodule:: fontTools.voltLib.ast .. automodule:: fontTools.voltLib.ast + :members: :undoc-members: -error ------ + +fontTools.voltLib.error +----------------------- + +.. currentmodule:: fontTools.voltLib.error .. automodule:: fontTools.voltLib.error + :members: :undoc-members: -lexer ------ + +fontTools.voltLib.lexer +----------------------- + +.. currentmodule:: fontTools.voltLib.lexer .. automodule:: fontTools.voltLib.lexer + :members: :undoc-members: -parser ------- + +fontTools.voltLib.parser +------------------------ + +.. currentmodule:: fontTools.voltLib.parser .. automodule:: fontTools.voltLib.parser + :members: :undoc-members: diff --git a/Doc/source/voltLib/voltToFea.rst b/Doc/source/voltLib/voltToFea.rst index 178dd68d5d..393d3abaa7 100644 --- a/Doc/source/voltLib/voltToFea.rst +++ b/Doc/source/voltLib/voltToFea.rst @@ -2,7 +2,8 @@ voltToFea: Convert MS VOLT to AFDKO feature files ################################################# +.. currentmodule:: fontTools.voltLib.voltToFea + .. automodule:: fontTools.voltLib.voltToFea - :inherited-members: :members: :undoc-members: diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index bc9f0e9055..6e41eb43a8 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -3,6 +3,6 @@ log = logging.getLogger(__name__) -version = __version__ = "4.57.0" +version = __version__ = "4.58.0" __all__ = ["version", "log", "configLogger"] diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index d75e23b750..4ad724a27a 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1464,10 +1464,11 @@ def _read(self, parent, value): if glyphName in allNames: # make up a new glyphName that's unique n = allNames[glyphName] - while (glyphName + "#" + str(n)) in allNames: + names = set(allNames) | set(charset) + while (glyphName + "." + str(n)) in names: n += 1 allNames[glyphName] = n + 1 - glyphName = glyphName + "#" + str(n) + glyphName = glyphName + "." + str(n) allNames[glyphName] = 1 newCharset.append(glyphName) charset = newCharset @@ -1663,25 +1664,26 @@ def _read(self, parent, value): return "StandardEncoding" elif value == 1: return "ExpertEncoding" + # custom encoding at offset `value` + assert value > 1 + file = parent.file + file.seek(value) + log.log(DEBUG, "loading Encoding at %s", value) + fmt = readCard8(file) + haveSupplement = bool(fmt & 0x80) + fmt = fmt & 0x7F + + if fmt == 0: + encoding = parseEncoding0(parent.charset, file) + elif fmt == 1: + encoding = parseEncoding1(parent.charset, file) else: - assert value > 1 - file = parent.file - file.seek(value) - log.log(DEBUG, "loading Encoding at %s", value) - fmt = readCard8(file) - haveSupplement = fmt & 0x80 - if haveSupplement: - raise NotImplementedError("Encoding supplements are not yet supported") - fmt = fmt & 0x7F - if fmt == 0: - encoding = parseEncoding0( - parent.charset, file, haveSupplement, parent.strings - ) - elif fmt == 1: - encoding = parseEncoding1( - parent.charset, file, haveSupplement, parent.strings - ) - return encoding + raise ValueError(f"Unknown Encoding format: {fmt}") + + if haveSupplement: + parseEncodingSupplement(file, encoding, parent.strings) + + return encoding def write(self, parent, value): if value == "StandardEncoding": @@ -1719,27 +1721,60 @@ def xmlRead(self, name, attrs, content, parent): return encoding -def parseEncoding0(charset, file, haveSupplement, strings): +def readSID(file): + """Read a String ID (SID) — 2-byte unsigned integer.""" + data = file.read(2) + if len(data) != 2: + raise EOFError("Unexpected end of file while reading SID") + return struct.unpack(">H", data)[0] # big-endian uint16 + + +def parseEncodingSupplement(file, encoding, strings): + """ + Parse the CFF Encoding supplement data: + - nSups: number of supplementary mappings + - each mapping: (code, SID) pair + and apply them to the `encoding` list in place. + """ + nSups = readCard8(file) + for _ in range(nSups): + code = readCard8(file) + sid = readSID(file) + name = strings[sid] + encoding[code] = name + + +def parseEncoding0(charset, file): + """ + Format 0: simple list of codes. + After reading the base table, optionally parse the supplement. + """ nCodes = readCard8(file) encoding = [".notdef"] * 256 for glyphID in range(1, nCodes + 1): code = readCard8(file) if code != 0: encoding[code] = charset[glyphID] + return encoding -def parseEncoding1(charset, file, haveSupplement, strings): +def parseEncoding1(charset, file): + """ + Format 1: range-based encoding. + After reading the base ranges, optionally parse the supplement. + """ nRanges = readCard8(file) encoding = [".notdef"] * 256 glyphID = 1 - for i in range(nRanges): + for _ in range(nRanges): code = readCard8(file) nLeft = readCard8(file) - for glyphID in range(glyphID, glyphID + nLeft + 1): + for _ in range(nLeft + 1): encoding[code] = charset[glyphID] - code = code + 1 - glyphID = glyphID + 1 + code += 1 + glyphID += 1 + return encoding diff --git a/Lib/fontTools/designspaceLib/statNames.py b/Lib/fontTools/designspaceLib/statNames.py index 1474e5fcf5..4e4f73470a 100644 --- a/Lib/fontTools/designspaceLib/statNames.py +++ b/Lib/fontTools/designspaceLib/statNames.py @@ -12,14 +12,13 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, Optional, Tuple, Union +from typing import Dict, Literal, Optional, Tuple, Union import logging from fontTools.designspaceLib import ( AxisDescriptor, AxisLabelDescriptor, DesignSpaceDocument, - DesignSpaceDocumentError, DiscreteAxisDescriptor, SimpleLocationDict, SourceDescriptor, @@ -27,9 +26,13 @@ LOGGER = logging.getLogger(__name__) -# TODO(Python 3.8): use Literal -# RibbiStyleName = Union[Literal["regular"], Literal["bold"], Literal["italic"], Literal["bold italic"]] -RibbiStyle = str +RibbiStyleName = Union[ + Literal["regular"], + Literal["bold"], + Literal["italic"], + Literal["bold italic"], +] + BOLD_ITALIC_TO_RIBBI_STYLE = { (False, False): "regular", (False, True): "italic", @@ -46,7 +49,7 @@ class StatNames: styleNames: Dict[str, str] postScriptFontName: Optional[str] styleMapFamilyNames: Dict[str, str] - styleMapStyleName: Optional[RibbiStyle] + styleMapStyleName: Optional[RibbiStyleName] def getStatNames( @@ -61,6 +64,10 @@ def getStatNames( localized names will be empty (family and style names), or the name will be None (PostScript name). + Note: this method does not consider info attached to the instance, like + family name. The user needs to override all names on an instance that STAT + information would compute differently than desired. + .. versionadded:: 5.0 """ familyNames: Dict[str, str] = {} @@ -201,7 +208,7 @@ def _getAxisLabelsForUserLocation( def _getRibbiStyle( self: DesignSpaceDocument, userLocation: SimpleLocationDict -) -> Tuple[RibbiStyle, SimpleLocationDict]: +) -> Tuple[RibbiStyleName, SimpleLocationDict]: """Compute the RIBBI style name of the given user location, return the location of the matching Regular in the RIBBI group. diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 10c49058c4..8479d7300d 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -337,6 +337,76 @@ def asFea(self, indent=""): return res +def _upgrade_mixed_subst_statements(statements): + # https://github.com/fonttools/fonttools/issues/612 + # A multiple substitution may have a single destination, in which case + # it will look just like a single substitution. So if there are both + # multiple and single substitutions, upgrade all the single ones to + # multiple substitutions. Similarly, a ligature substitution may have a + # single source glyph, so if there are both ligature and single + # substitutions, upgrade all the single ones to ligature substitutions. + + has_single = False + has_multiple = False + has_ligature = False + for s in statements: + if isinstance(s, SingleSubstStatement): + has_single = not any([s.prefix, s.suffix, s.forceChain]) + elif isinstance(s, MultipleSubstStatement): + has_multiple = not any([s.prefix, s.suffix, s.forceChain]) + elif isinstance(s, LigatureSubstStatement): + has_ligature = not any([s.prefix, s.suffix, s.forceChain]) + + to_multiple = False + to_ligature = False + + # If we have mixed single and multiple substitutions, + # upgrade all single substitutions to multiple substitutions. + if has_single and has_multiple and not has_ligature: + to_multiple = True + + # If we have mixed single and ligature substitutions, + # upgrade all single substitutions to ligature substitutions. + elif has_single and has_ligature and not has_multiple: + to_ligature = True + + if to_multiple or to_ligature: + ret = [] + for s in statements: + if isinstance(s, SingleSubstStatement): + glyphs = s.glyphs[0].glyphSet() + replacements = s.replacements[0].glyphSet() + if len(replacements) == 1: + replacements *= len(glyphs) + for glyph, replacement in zip(glyphs, replacements): + if to_multiple: + ret.append( + MultipleSubstStatement( + s.prefix, + glyph, + s.suffix, + [replacement], + s.forceChain, + location=s.location, + ) + ) + elif to_ligature: + ret.append( + LigatureSubstStatement( + s.prefix, + [GlyphName(glyph)], + s.suffix, + replacement, + s.forceChain, + location=s.location, + ) + ) + else: + ret.append(s) + return ret + return statements + + class Block(Statement): """A block of statements: feature, lookup, etc.""" @@ -348,7 +418,8 @@ def build(self, builder): """When handed a 'builder' object of comparable interface to :class:`fontTools.feaLib.builder`, walks the statements in this block, calling the builder callbacks.""" - for s in self.statements: + statements = _upgrade_mixed_subst_statements(self.statements) + for s in statements: s.build(builder) def asFea(self, indent=""): @@ -382,8 +453,7 @@ def __init__(self, name, use_extension=False, location=None): def build(self, builder): """Call the ``start_feature`` callback on the builder object, visit all the statements in this feature, and then call ``end_feature``.""" - # TODO(sascha): Handle use_extension. - builder.start_feature(self.location, self.name) + builder.start_feature(self.location, self.name, self.use_extension) # language exclude_dflt statements modify builder.features_ # limit them to this block with temporary builder.features_ features = builder.features_ @@ -433,8 +503,7 @@ def __init__(self, name, use_extension=False, location=None): self.name, self.use_extension = name, use_extension def build(self, builder): - # TODO(sascha): Handle use_extension. - builder.start_lookup_block(self.location, self.name) + builder.start_lookup_block(self.location, self.name, self.use_extension) Block.build(self, builder) builder.end_lookup_block() @@ -753,7 +822,7 @@ def asFea(self, indent=""): if len(self.suffix): res += " " + " ".join(map(asFea, self.suffix)) else: - res += " ".join(map(asFea, self.glyph)) + res += " ".join(map(asFea, self.glyphs)) res += ";" return res @@ -811,7 +880,7 @@ def asFea(self, indent=""): if len(self.suffix): res += " " + " ".join(map(asFea, self.suffix)) else: - res += " ".join(map(asFea, self.glyph)) + res += " ".join(map(asFea, self.glyphs)) res += ";" return res @@ -1512,7 +1581,9 @@ def asFea(self, indent=""): res += " ".join(map(asFea, self.prefix)) + " " res += " ".join( [ - asFea(x[0]) + "'" + ((" " + x[1].asFea()) if x[1] else "") + asFea(x[0]) + + "'" + + ((" " + x[1].asFea()) if x[1] is not None else "") for x in self.pos ] ) @@ -1520,7 +1591,10 @@ def asFea(self, indent=""): res += " " + " ".join(map(asFea, self.suffix)) else: res += " ".join( - [asFea(x[0]) + " " + (x[1].asFea() if x[1] else "") for x in self.pos] + [ + asFea(x[0]) + " " + (x[1].asFea() if x[1] is not None else "") + for x in self.pos + ] ) res += ";" return res @@ -2103,7 +2177,7 @@ def __init__(self, name, conditionset, use_extension=False, location=None): def build(self, builder): """Call the ``start_feature`` callback on the builder object, visit all the statements in this feature, and then call ``end_feature``.""" - builder.start_feature(self.location, self.name) + builder.start_feature(self.location, self.name, self.use_extension) if ( self.conditionset != "NULL" and self.conditionset not in builder.conditionsets_ diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 8b2c7208b2..1583f06d9e 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -126,6 +126,7 @@ def __init__(self, font, featurefile): self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None + self.use_extension_ = False self.language_systems = set() self.seen_non_DFLT_script_ = False self.named_lookups_ = {} @@ -141,6 +142,7 @@ def __init__(self, font, featurefile): self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_location_ = None self.aalt_alternates_ = {} + self.aalt_use_extension_ = False # for 'featureNames' self.featureNames_ = set() self.featureNames_ids_ = {} @@ -247,6 +249,7 @@ def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) result.lookupflag = self.lookupflag_ result.markFilterSet = self.lookupflag_markFilterSet_ + result.extension = self.use_extension_ self.lookups_.append(result) return result @@ -272,6 +275,7 @@ def get_lookup_(self, location, builder_class): self.cur_lookup_ = builder_class(self.font, location) self.cur_lookup_.lookupflag = self.lookupflag_ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ + self.cur_lookup_.extension = self.use_extension_ self.lookups_.append(self.cur_lookup_) if self.cur_lookup_name_: # We are starting a lookup rule inside a named lookup block. @@ -323,7 +327,7 @@ def build_feature_aalt_(self): } old_lookups = self.lookups_ self.lookups_ = [] - self.start_feature(self.aalt_location_, "aalt") + self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_) if single: single_lookup = self.get_lookup_(location, SingleSubstBuilder) single_lookup.mapping = single @@ -1054,15 +1058,22 @@ def get_default_language_systems_(self): else: return frozenset({("DFLT", "dflt")}) - def start_feature(self, location, name): + def start_feature(self, location, name, use_extension=False): + if use_extension and name != "aalt": + raise FeatureLibError( + "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature", + location, + ) self.language_systems = self.get_default_language_systems_() self.script_ = "DFLT" self.cur_lookup_ = None self.cur_feature_name_ = name self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None + self.use_extension_ = use_extension if name == "aalt": self.aalt_location_ = location + self.aalt_use_extension_ = use_extension def end_feature(self): assert self.cur_feature_name_ is not None @@ -1071,8 +1082,9 @@ def end_feature(self): self.cur_lookup_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None + self.use_extension_ = False - def start_lookup_block(self, location, name): + def start_lookup_block(self, location, name, use_extension=False): if name in self.named_lookups_: raise FeatureLibError( 'Lookup "%s" has already been defined' % name, location @@ -1086,6 +1098,7 @@ def start_lookup_block(self, location, name): self.cur_lookup_name_ = name self.named_lookups_[name] = None self.cur_lookup_ = None + self.use_extension_ = use_extension if self.cur_feature_name_ is None: self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None @@ -1094,6 +1107,7 @@ def end_lookup_block(self): assert self.cur_lookup_name_ is not None self.cur_lookup_name_ = None self.cur_lookup_ = None + self.use_extension_ = False if self.cur_feature_name_ is None: self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None @@ -1471,7 +1485,9 @@ def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2) lookup = self.get_lookup_(location, PairPosBuilder) v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True) v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True) - lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2) + cls1 = tuple(sorted(set(glyphclass1))) + cls2 = tuple(sorted(set(glyphclass2))) + lookup.addClassPair(location, cls1, v1, cls2, v2) def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): if not glyph1 or not glyph2: diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 5f647ca0ac..451dd62411 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -1613,7 +1613,7 @@ def parse_base_script_list_(self, count): "HorizAxis.BaseScriptList", "VertAxis.BaseScriptList", ), self.cur_token_ - scripts = [(self.parse_base_script_record_(count))] + scripts = [self.parse_base_script_record_(count)] while self.next_token_ == ",": self.expect_symbol_(",") scripts.append(self.parse_base_script_record_(count)) @@ -2062,44 +2062,6 @@ def parse_block_( ) self.expect_symbol_(";") - # A multiple substitution may have a single destination, in which case - # it will look just like a single substitution. So if there are both - # multiple and single substitutions, upgrade all the single ones to - # multiple substitutions. - - # Check if we have a mix of non-contextual singles and multiples. - has_single = False - has_multiple = False - for s in statements: - if isinstance(s, self.ast.SingleSubstStatement): - has_single = not any([s.prefix, s.suffix, s.forceChain]) - elif isinstance(s, self.ast.MultipleSubstStatement): - has_multiple = not any([s.prefix, s.suffix, s.forceChain]) - - # Upgrade all single substitutions to multiple substitutions. - if has_single and has_multiple: - statements = [] - for s in block.statements: - if isinstance(s, self.ast.SingleSubstStatement): - glyphs = s.glyphs[0].glyphSet() - replacements = s.replacements[0].glyphSet() - if len(replacements) == 1: - replacements *= len(glyphs) - for i, glyph in enumerate(glyphs): - statements.append( - self.ast.MultipleSubstStatement( - s.prefix, - glyph, - s.suffix, - [replacements[i]], - s.forceChain, - location=s.location, - ) - ) - else: - statements.append(s) - block.statements = statements - def is_cur_keyword_(self, k): if self.cur_token_type_ is Lexer.NAME: if isinstance(k, type("")): # basestring is gone in Python3 diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index d4af38fba4..f8da717bab 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -714,6 +714,12 @@ def setupGvar(self, variations): gvar.reserved = 0 gvar.variations = variations + def setupGVAR(self, variations): + gvar = self.font["GVAR"] = newTable("GVAR") + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = variations + def calcGlyphBounds(self): """Calculate the bounding boxes of all glyphs in the `glyf` table. This is usually not called explicitly by client code. diff --git a/Lib/fontTools/misc/etree.py b/Lib/fontTools/misc/etree.py index d0967b5f52..743546061c 100644 --- a/Lib/fontTools/misc/etree.py +++ b/Lib/fontTools/misc/etree.py @@ -56,21 +56,7 @@ from xml.etree.ElementTree import * _have_lxml = False - import sys - - # dict is always ordered in python >= 3.6 and on pypy - PY36 = sys.version_info >= (3, 6) - try: - import __pypy__ - except ImportError: - __pypy__ = None - _dict_is_ordered = bool(PY36 or __pypy__) - del PY36, __pypy__ - - if _dict_is_ordered: - _Attrib = dict - else: - from collections import OrderedDict as _Attrib + _Attrib = dict if isinstance(Element, type): _Element = Element @@ -221,18 +207,9 @@ def tostring( # characters, the surrogate blocks, FFFE, and FFFF: # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] # Here we reversed the pattern to match only the invalid characters. - # For the 'narrow' python builds supporting only UCS-2, which represent - # characters beyond BMP as UTF-16 surrogate pairs, we need to pass through - # the surrogate block. I haven't found a more elegant solution... - UCS2 = sys.maxunicode < 0x10FFFF - if UCS2: - _invalid_xml_string = re.compile( - "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uFFFE-\uFFFF]" - ) - else: - _invalid_xml_string = re.compile( - "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]" - ) + _invalid_xml_string = re.compile( + "[\u0000-\u0008\u000B-\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE-\uFFFF]" + ) def _tounicode(s): """Test if a string is valid user input and decode it to unicode string diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py index dbedf275e3..e797be375b 100644 --- a/Lib/fontTools/mtiLib/__init__.py +++ b/Lib/fontTools/mtiLib/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - # FontDame-to-FontTools for OpenType Layout tables # # Source language spec is available at: diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index b944ea8c26..064b2fce31 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1,6 +1,8 @@ +from __future__ import annotations + from collections import namedtuple, OrderedDict import itertools -import os +from typing import Dict, Union from fontTools.misc.fixedTools import fixedToFloat from fontTools.misc.roundTools import otRound from fontTools import ttLib @@ -10,15 +12,15 @@ valueRecordFormatDict, OTLOffsetOverflowError, OTTableWriter, - CountReference, ) -from fontTools.ttLib.tables import otBase +from fontTools.ttLib.ttFont import TTFont from fontTools.feaLib.ast import STATNameStatement from fontTools.otlLib.optimize.gpos import ( _compression_level_from_env, compact_lookup, ) from fontTools.otlLib.error import OpenTypeLibError +from fontTools.misc.loggingTools import deprecateFunction from functools import reduce import logging import copy @@ -73,7 +75,7 @@ def buildCoverage(glyphs, glyphMap): LOOKUP_FLAG_USE_MARK_FILTERING_SET = 0x0010 -def buildLookup(subtables, flags=0, markFilterSet=None): +def buildLookup(subtables, flags=0, markFilterSet=None, table=None, extension=False): """Turns a collection of rules into a lookup. A Lookup (as defined in the `OpenType Spec `__) @@ -98,6 +100,8 @@ def buildLookup(subtables, flags=0, markFilterSet=None): lookup. If a mark filtering set is provided, `LOOKUP_FLAG_USE_MARK_FILTERING_SET` will be set on the lookup's flags. + table (str): The name of the table this lookup belongs to, e.g. "GPOS" or "GSUB". + extension (bool): ``True`` if this is an extension lookup, ``False`` otherwise. Returns: An ``otTables.Lookup`` object or ``None`` if there are no subtables @@ -113,8 +117,21 @@ def buildLookup(subtables, flags=0, markFilterSet=None): ), "all subtables must have the same LookupType; got %s" % repr( [t.LookupType for t in subtables] ) + + if extension: + assert table in ("GPOS", "GSUB") + lookupType = 7 if table == "GSUB" else 9 + extSubTableClass = ot.lookupTypes[table][lookupType] + for i, st in enumerate(subtables): + subtables[i] = extSubTableClass() + subtables[i].Format = 1 + subtables[i].ExtSubTable = st + subtables[i].ExtensionLookupType = st.LookupType + else: + lookupType = subtables[0].LookupType + self = ot.Lookup() - self.LookupType = subtables[0].LookupType + self.LookupType = lookupType self.LookupFlag = flags self.SubTable = subtables self.SubTableCount = len(self.SubTable) @@ -133,7 +150,7 @@ def buildLookup(subtables, flags=0, markFilterSet=None): class LookupBuilder(object): SUBTABLE_BREAK_ = "SUBTABLE_BREAK" - def __init__(self, font, location, table, lookup_type): + def __init__(self, font, location, table, lookup_type, extension=False): self.font = font self.glyphMap = font.getReverseGlyphMap() self.location = location @@ -141,6 +158,7 @@ def __init__(self, font, location, table, lookup_type): self.lookupflag = 0 self.markFilterSet = None self.lookup_index = None # assigned when making final tables + self.extension = extension assert table in ("GPOS", "GSUB") def equals(self, other): @@ -149,6 +167,7 @@ def equals(self, other): and self.table == other.table and self.lookupflag == other.lookupflag and self.markFilterSet == other.markFilterSet + and self.extension == other.extension ) def inferGlyphClasses(self): @@ -160,7 +179,13 @@ def getAlternateGlyphs(self): return {} def buildLookup_(self, subtables): - return buildLookup(subtables, self.lookupflag, self.markFilterSet) + return buildLookup( + subtables, + self.lookupflag, + self.markFilterSet, + self.table, + self.extension, + ) def buildMarkClasses_(self, marks): """{"cedilla": ("BOTTOM", ast.Anchor), ...} --> {"BOTTOM":0, "TOP":1} @@ -949,8 +974,20 @@ def build(self): An ``otTables.Lookup`` object representing the cursive positioning lookup. """ - st = buildCursivePosSubtable(self.attachments, self.glyphMap) - return self.buildLookup_([st]) + attachments = [{}] + for key in self.attachments: + if key[0] == self.SUBTABLE_BREAK_: + attachments.append({}) + else: + attachments[-1][key] = self.attachments[key] + subtables = [buildCursivePosSubtable(s, self.glyphMap) for s in attachments] + return self.buildLookup_(subtables) + + def add_subtable_break(self, location): + self.attachments[(self.SUBTABLE_BREAK_, location)] = ( + self.SUBTABLE_BREAK_, + self.SUBTABLE_BREAK_, + ) class MarkBasePosBuilder(LookupBuilder): @@ -985,17 +1022,25 @@ def __init__(self, font, location): LookupBuilder.__init__(self, font, location, "GPOS", 4) self.marks = {} # glyphName -> (markClassName, anchor) self.bases = {} # glyphName -> {markClassName: anchor} + self.subtables_ = [] + + def get_subtables_(self): + subtables_ = self.subtables_ + if self.bases or self.marks: + subtables_.append((self.marks, self.bases)) + return subtables_ def equals(self, other): return ( LookupBuilder.equals(self, other) - and self.marks == other.marks - and self.bases == other.bases + and self.get_subtables_() == other.get_subtables_() ) def inferGlyphClasses(self): - result = {glyph: 1 for glyph in self.bases} - result.update({glyph: 3 for glyph in self.marks}) + result = {} + for marks, bases in self.get_subtables_(): + result.update({glyph: 1 for glyph in bases}) + result.update({glyph: 3 for glyph in marks}) return result def build(self): @@ -1005,26 +1050,33 @@ def build(self): An ``otTables.Lookup`` object representing the mark-to-base positioning lookup. """ - markClasses = self.buildMarkClasses_(self.marks) - marks = {} - for mark, (mc, anchor) in self.marks.items(): - if mc not in markClasses: - raise ValueError( - "Mark class %s not found for mark glyph %s" % (mc, mark) - ) - marks[mark] = (markClasses[mc], anchor) - bases = {} - for glyph, anchors in self.bases.items(): - bases[glyph] = {} - for mc, anchor in anchors.items(): + subtables = [] + for subtable in self.get_subtables_(): + markClasses = self.buildMarkClasses_(subtable[0]) + marks = {} + for mark, (mc, anchor) in subtable[0].items(): if mc not in markClasses: raise ValueError( - "Mark class %s not found for base glyph %s" % (mc, glyph) + "Mark class %s not found for mark glyph %s" % (mc, mark) ) - bases[glyph][markClasses[mc]] = anchor - subtables = buildMarkBasePos(marks, bases, self.glyphMap) + marks[mark] = (markClasses[mc], anchor) + bases = {} + for glyph, anchors in subtable[1].items(): + bases[glyph] = {} + for mc, anchor in anchors.items(): + if mc not in markClasses: + raise ValueError( + "Mark class %s not found for base glyph %s" % (mc, glyph) + ) + bases[glyph][markClasses[mc]] = anchor + subtables.append(buildMarkBasePosSubtable(marks, bases, self.glyphMap)) return self.buildLookup_(subtables) + def add_subtable_break(self, location): + self.subtables_.append((self.marks, self.bases)) + self.marks = {} + self.bases = {} + class MarkLigPosBuilder(LookupBuilder): """Builds a Mark-To-Ligature Positioning (GPOS5) lookup. @@ -1061,17 +1113,25 @@ def __init__(self, font, location): LookupBuilder.__init__(self, font, location, "GPOS", 5) self.marks = {} # glyphName -> (markClassName, anchor) self.ligatures = {} # glyphName -> [{markClassName: anchor}, ...] + self.subtables_ = [] + + def get_subtables_(self): + subtables_ = self.subtables_ + if self.ligatures or self.marks: + subtables_.append((self.marks, self.ligatures)) + return subtables_ def equals(self, other): return ( LookupBuilder.equals(self, other) - and self.marks == other.marks - and self.ligatures == other.ligatures + and self.get_subtables_() == other.get_subtables_() ) def inferGlyphClasses(self): - result = {glyph: 2 for glyph in self.ligatures} - result.update({glyph: 3 for glyph in self.marks}) + result = {} + for marks, ligatures in self.get_subtables_(): + result.update({glyph: 2 for glyph in ligatures}) + result.update({glyph: 3 for glyph in marks}) return result def build(self): @@ -1081,18 +1141,26 @@ def build(self): An ``otTables.Lookup`` object representing the mark-to-ligature positioning lookup. """ - markClasses = self.buildMarkClasses_(self.marks) - marks = { - mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() - } - ligs = {} - for lig, components in self.ligatures.items(): - ligs[lig] = [] - for c in components: - ligs[lig].append({markClasses[mc]: a for mc, a in c.items()}) - subtables = buildMarkLigPos(marks, ligs, self.glyphMap) + subtables = [] + for subtable in self.get_subtables_(): + markClasses = self.buildMarkClasses_(subtable[0]) + marks = { + mark: (markClasses[mc], anchor) + for mark, (mc, anchor) in subtable[0].items() + } + ligs = {} + for lig, components in subtable[1].items(): + ligs[lig] = [] + for c in components: + ligs[lig].append({markClasses[mc]: a for mc, a in c.items()}) + subtables.append(buildMarkLigPosSubtable(marks, ligs, self.glyphMap)) return self.buildLookup_(subtables) + def add_subtable_break(self, location): + self.subtables_.append((self.marks, self.ligatures)) + self.marks = {} + self.ligatures = {} + class MarkMarkPosBuilder(LookupBuilder): """Builds a Mark-To-Mark Positioning (GPOS6) lookup. @@ -1125,17 +1193,25 @@ def __init__(self, font, location): LookupBuilder.__init__(self, font, location, "GPOS", 6) self.marks = {} # glyphName -> (markClassName, anchor) self.baseMarks = {} # glyphName -> {markClassName: anchor} + self.subtables_ = [] + + def get_subtables_(self): + subtables_ = self.subtables_ + if self.baseMarks or self.marks: + subtables_.append((self.marks, self.baseMarks)) + return subtables_ def equals(self, other): return ( LookupBuilder.equals(self, other) - and self.marks == other.marks - and self.baseMarks == other.baseMarks + and self.get_subtables_() == other.get_subtables_() ) def inferGlyphClasses(self): - result = {glyph: 3 for glyph in self.baseMarks} - result.update({glyph: 3 for glyph in self.marks}) + result = {} + for marks, baseMarks in self.get_subtables_(): + result.update({glyph: 3 for glyph in baseMarks}) + result.update({glyph: 3 for glyph in marks}) return result def build(self): @@ -1145,25 +1221,34 @@ def build(self): An ``otTables.Lookup`` object representing the mark-to-mark positioning lookup. """ - markClasses = self.buildMarkClasses_(self.marks) - markClassList = sorted(markClasses.keys(), key=markClasses.get) - marks = { - mark: (markClasses[mc], anchor) for mark, (mc, anchor) in self.marks.items() - } + subtables = [] + for subtable in self.get_subtables_(): + markClasses = self.buildMarkClasses_(subtable[0]) + markClassList = sorted(markClasses.keys(), key=markClasses.get) + marks = { + mark: (markClasses[mc], anchor) + for mark, (mc, anchor) in subtable[0].items() + } + + st = ot.MarkMarkPos() + st.Format = 1 + st.ClassCount = len(markClasses) + st.Mark1Coverage = buildCoverage(marks, self.glyphMap) + st.Mark2Coverage = buildCoverage(subtable[1], self.glyphMap) + st.Mark1Array = buildMarkArray(marks, self.glyphMap) + st.Mark2Array = ot.Mark2Array() + st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs) + st.Mark2Array.Mark2Record = [] + for base in st.Mark2Coverage.glyphs: + anchors = [subtable[1][base].get(mc) for mc in markClassList] + st.Mark2Array.Mark2Record.append(buildMark2Record(anchors)) + subtables.append(st) + return self.buildLookup_(subtables) - st = ot.MarkMarkPos() - st.Format = 1 - st.ClassCount = len(markClasses) - st.Mark1Coverage = buildCoverage(marks, self.glyphMap) - st.Mark2Coverage = buildCoverage(self.baseMarks, self.glyphMap) - st.Mark1Array = buildMarkArray(marks, self.glyphMap) - st.Mark2Array = ot.Mark2Array() - st.Mark2Array.Mark2Count = len(st.Mark2Coverage.glyphs) - st.Mark2Array.Mark2Record = [] - for base in st.Mark2Coverage.glyphs: - anchors = [self.baseMarks[base].get(mc) for mc in markClassList] - st.Mark2Array.Mark2Record.append(buildMark2Record(anchors)) - return self.buildLookup_([st]) + def add_subtable_break(self, location): + self.subtables_.append((self.marks, self.baseMarks)) + self.marks = {} + self.baseMarks = {} class ReverseChainSingleSubstBuilder(LookupBuilder): @@ -1484,6 +1569,8 @@ def add_pos(self, location, glyph, otValueRecord): otValueRection: A ``otTables.ValueRecord`` used to position the glyph. """ + if otValueRecord is None: + otValueRecord = ValueRecord() if not self.can_add(glyph, otValueRecord): otherLoc = self.locations[glyph] raise OpenTypeLibError( @@ -1900,53 +1987,15 @@ def buildMarkArray(marks, glyphMap): return self +@deprecateFunction( + "use buildMarkBasePosSubtable() instead", category=DeprecationWarning +) def buildMarkBasePos(marks, bases, glyphMap): """Build a list of MarkBasePos (GPOS4) subtables. - This routine turns a set of marks and bases into a list of mark-to-base - positioning subtables. Currently the list will contain a single subtable - containing all marks and bases, although at a later date it may return the - optimal list of subtables subsetting the marks and bases into groups which - save space. See :func:`buildMarkBasePosSubtable` below. - - Note that if you are implementing a layout compiler, you may find it more - flexible to use - :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead. - - Example:: - - # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... - - marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} - bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} - markbaseposes = buildMarkBasePos(marks, bases, font.getReverseGlyphMap()) - - Args: - marks (dict): A dictionary mapping anchors to glyphs; the keys being - glyph names, and the values being a tuple of mark class number and - an ``otTables.Anchor`` object representing the mark's attachment - point. (See :func:`buildMarkArray`.) - bases (dict): A dictionary mapping anchors to glyphs; the keys being - glyph names, and the values being dictionaries mapping mark class ID - to the appropriate ``otTables.Anchor`` object used for attaching marks - of that class. (See :func:`buildBaseArray`.) - glyphMap: a glyph name to ID map, typically returned from - ``font.getReverseGlyphMap()``. - - Returns: - A list of ``otTables.MarkBasePos`` objects. + .. deprecated:: 4.58.0 + Use :func:`buildMarkBasePosSubtable` instead. """ - # TODO: Consider emitting multiple subtables to save space. - # Partition the marks and bases into disjoint subsets, so that - # MarkBasePos rules would only access glyphs from a single - # subset. This would likely lead to smaller mark/base - # matrices, so we might be able to omit many of the empty - # anchor tables that we currently produce. Of course, this - # would only work if the MarkBasePos rules of real-world fonts - # allow partitioning into multiple subsets. We should find out - # whether this is the case; if so, implement the optimization. - # On the other hand, a very large number of subtables could - # slow down layout engines; so this would need profiling. return [buildMarkBasePosSubtable(marks, bases, glyphMap)] @@ -1954,7 +2003,15 @@ def buildMarkBasePosSubtable(marks, bases, glyphMap): """Build a single MarkBasePos (GPOS4) subtable. This builds a mark-to-base lookup subtable containing all of the referenced - marks and bases. See :func:`buildMarkBasePos`. + marks and bases. + + Example:: + + # a1, a2, a3, a4, a5 = buildAnchor(500, 100), ... + + marks = {"acute": (0, a1), "grave": (0, a1), "cedilla": (1, a2)} + bases = {"a": {0: a3, 1: a5}, "b": {0: a4, 1: a5}} + markbaseposes = [buildMarkBasePosSubtable(marks, bases, font.getReverseGlyphMap())] Args: marks (dict): A dictionary mapping anchors to glyphs; the keys being @@ -1981,14 +2038,21 @@ def buildMarkBasePosSubtable(marks, bases, glyphMap): return self +@deprecateFunction("use buildMarkLigPosSubtable() instead", category=DeprecationWarning) def buildMarkLigPos(marks, ligs, glyphMap): """Build a list of MarkLigPos (GPOS5) subtables. - This routine turns a set of marks and ligatures into a list of mark-to-ligature - positioning subtables. Currently the list will contain a single subtable - containing all marks and ligatures, although at a later date it may return - the optimal list of subtables subsetting the marks and ligatures into groups - which save space. See :func:`buildMarkLigPosSubtable` below. + .. deprecated:: 4.58.0 + Use :func:`buildMarkLigPosSubtable` instead. + """ + return [buildMarkLigPosSubtable(marks, ligs, glyphMap)] + + +def buildMarkLigPosSubtable(marks, ligs, glyphMap): + """Build a single MarkLigPos (GPOS5) subtable. + + This builds a mark-to-base lookup subtable containing all of the referenced + marks and bases. Note that if you are implementing a layout compiler, you may find it more flexible to use @@ -2009,37 +2073,9 @@ def buildMarkLigPos(marks, ligs, glyphMap): ], # "c_t": [{...}, {...}] } - markligposes = buildMarkLigPos(marks, ligs, + markligpose = buildMarkLigPosSubtable(marks, ligs, font.getReverseGlyphMap()) - Args: - marks (dict): A dictionary mapping anchors to glyphs; the keys being - glyph names, and the values being a tuple of mark class number and - an ``otTables.Anchor`` object representing the mark's attachment - point. (See :func:`buildMarkArray`.) - ligs (dict): A mapping of ligature names to an array of dictionaries: - for each component glyph in the ligature, an dictionary mapping - mark class IDs to anchors. (See :func:`buildLigatureArray`.) - glyphMap: a glyph name to ID map, typically returned from - ``font.getReverseGlyphMap()``. - - Returns: - A list of ``otTables.MarkLigPos`` objects. - - """ - # TODO: Consider splitting into multiple subtables to save space, - # as with MarkBasePos, this would be a trade-off that would need - # profiling. And, depending on how typical fonts are structured, - # it might not be worth doing at all. - return [buildMarkLigPosSubtable(marks, ligs, glyphMap)] - - -def buildMarkLigPosSubtable(marks, ligs, glyphMap): - """Build a single MarkLigPos (GPOS5) subtable. - - This builds a mark-to-base lookup subtable containing all of the referenced - marks and bases. See :func:`buildMarkLigPos`. - Args: marks (dict): A dictionary mapping anchors to glyphs; the keys being glyph names, and the values being a tuple of mark class number and @@ -2706,10 +2742,18 @@ def build(self): AXIS_VALUE_NEGATIVE_INFINITY = fixedToFloat(-0x80000000, 16) AXIS_VALUE_POSITIVE_INFINITY = fixedToFloat(0x7FFFFFFF, 16) +STATName = Union[int, str, Dict[str, str]] +"""A raw name ID, English name, or multilingual name.""" + def buildStatTable( - ttFont, axes, locations=None, elidedFallbackName=2, windowsNames=True, macNames=True -): + ttFont: TTFont, + axes, + locations=None, + elidedFallbackName: Union[STATName, STATNameStatement] = 2, + windowsNames: bool = True, + macNames: bool = True, +) -> None: """Add a 'STAT' table to 'ttFont'. 'axes' is a list of dictionaries describing axes and their @@ -2900,7 +2944,13 @@ def _buildAxisValuesFormat4(locations, axes, ttFont, windowsNames=True, macNames return axisValues -def _addName(ttFont, value, minNameID=0, windows=True, mac=True): +def _addName( + ttFont: TTFont, + value: Union[STATName, STATNameStatement], + minNameID: int = 0, + windows: bool = True, + mac: bool = True, +) -> int: nameTable = ttFont["name"] if isinstance(value, int): # Already a nameID diff --git a/Lib/fontTools/otlLib/optimize/gpos.py b/Lib/fontTools/otlLib/optimize/gpos.py index 61ea856d96..3edbfeb306 100644 --- a/Lib/fontTools/otlLib/optimize/gpos.py +++ b/Lib/fontTools/otlLib/optimize/gpos.py @@ -1,7 +1,8 @@ import logging import os from collections import defaultdict, namedtuple -from functools import reduce +from dataclasses import dataclass +from functools import cached_property, reduce from itertools import chain from math import log2 from typing import DefaultDict, Dict, Iterable, List, Sequence, Tuple @@ -192,79 +193,58 @@ def _classDef_bytes( ) +@dataclass class Cluster: - # TODO(Python 3.7): Turn this into a dataclass - # ctx: ClusteringContext - # indices: int - # Caches - # TODO(Python 3.8): use functools.cached_property instead of the - # manually cached properties, and remove the cache fields listed below. - # _indices: Optional[List[int]] = None - # _column_indices: Optional[List[int]] = None - # _cost: Optional[int] = None - - __slots__ = "ctx", "indices_bitmask", "_indices", "_column_indices", "_cost" - - def __init__(self, ctx: ClusteringContext, indices_bitmask: int): - self.ctx = ctx - self.indices_bitmask = indices_bitmask - self._indices = None - self._column_indices = None - self._cost = None + ctx: ClusteringContext + indices_bitmask: int - @property + @cached_property def indices(self): - if self._indices is None: - self._indices = bit_indices(self.indices_bitmask) - return self._indices + return bit_indices(self.indices_bitmask) - @property + @cached_property def column_indices(self): - if self._column_indices is None: - # Indices of columns that have a 1 in at least 1 line - # => binary OR all the lines - bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices)) - self._column_indices = bit_indices(bitmask) - return self._column_indices + # Indices of columns that have a 1 in at least 1 line + # => binary OR all the lines + bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices)) + return bit_indices(bitmask) @property def width(self): # Add 1 because Class2=0 cannot be used but needs to be encoded. return len(self.column_indices) + 1 - @property + @cached_property def cost(self): - if self._cost is None: - self._cost = ( - # 2 bytes to store the offset to this subtable in the Lookup table above - 2 - # Contents of the subtable - # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment - # uint16 posFormat Format identifier: format = 2 - + 2 - # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable. - + 2 - + self.coverage_bytes - # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero). - + 2 - # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero). - + 2 - # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair. - + 2 - + self.classDef1_bytes - # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair. - + 2 - + self.classDef2_bytes - # uint16 class1Count Number of classes in classDef1 table — includes Class 0. - + 2 - # uint16 class2Count Number of classes in classDef2 table — includes Class 0. - + 2 - # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1. - + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes) - * len(self.indices) - * self.width - ) - return self._cost + return ( + # 2 bytes to store the offset to this subtable in the Lookup table above + 2 + # Contents of the subtable + # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment + # uint16 posFormat Format identifier: format = 2 + + 2 + # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable. + + 2 + + self.coverage_bytes + # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero). + + 2 + # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero). + + 2 + # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair. + + 2 + + self.classDef1_bytes + # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair. + + 2 + + self.classDef2_bytes + # uint16 class1Count Number of classes in classDef1 table — includes Class 0. + + 2 + # uint16 class2Count Number of classes in classDef2 table — includes Class 0. + + 2 + # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1. + + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes) + * len(self.indices) + * self.width + ) @property def coverage_bytes(self): diff --git a/Lib/fontTools/pens/pointPen.py b/Lib/fontTools/pens/pointPen.py index 93a9201c99..843d7a28d3 100644 --- a/Lib/fontTools/pens/pointPen.py +++ b/Lib/fontTools/pens/pointPen.py @@ -12,12 +12,14 @@ For instance, whether or not a point is smooth, and its name. """ +from __future__ import annotations + import math -from typing import Any, Optional, Tuple, Dict +from typing import Any, Dict, List, Optional, Tuple from fontTools.misc.loggingTools import LogMixin -from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError from fontTools.misc.transform import DecomposedTransform, Identity +from fontTools.pens.basePen import AbstractPen, MissingComponentError, PenError __all__ = [ "AbstractPointPen", @@ -28,6 +30,14 @@ "ReverseContourPointPen", ] +# Some type aliases to make it easier below +Point = Tuple[float, float] +PointName = Optional[str] +# [(pt, smooth, name, kwargs)] +SegmentPointList = List[Tuple[Optional[Point], bool, PointName, Any]] +SegmentType = Optional[str] +SegmentList = List[Tuple[SegmentType, SegmentPointList]] + class AbstractPointPen: """Baseclass for all PointPens.""" @@ -88,7 +98,7 @@ class BasePointToSegmentPen(AbstractPointPen): care of all the edge cases. """ - def __init__(self): + def __init__(self) -> None: self.currentPath = None def beginPath(self, identifier=None, **kwargs): @@ -96,7 +106,7 @@ def beginPath(self, identifier=None, **kwargs): raise PenError("Path already begun.") self.currentPath = [] - def _flushContour(self, segments): + def _flushContour(self, segments: SegmentList) -> None: """Override this method. It will be called for each non-empty sub path with a list @@ -124,7 +134,7 @@ def _flushContour(self, segments): """ raise NotImplementedError - def endPath(self): + def endPath(self) -> None: if self.currentPath is None: raise PenError("Path not begun.") points = self.currentPath @@ -134,7 +144,7 @@ def endPath(self): if len(points) == 1: # Not much more we can do than output a single move segment. pt, segmentType, smooth, name, kwargs = points[0] - segments = [("move", [(pt, smooth, name, kwargs)])] + segments: SegmentList = [("move", [(pt, smooth, name, kwargs)])] self._flushContour(segments) return segments = [] @@ -162,7 +172,7 @@ def endPath(self): else: points = points[firstOnCurve + 1 :] + points[: firstOnCurve + 1] - currentSegment = [] + currentSegment: SegmentPointList = [] for pt, segmentType, smooth, name, kwargs in points: currentSegment.append((pt, smooth, name, kwargs)) if segmentType is None: @@ -189,7 +199,7 @@ class PointToSegmentPen(BasePointToSegmentPen): and kwargs. """ - def __init__(self, segmentPen, outputImpliedClosingLine=False): + def __init__(self, segmentPen, outputImpliedClosingLine: bool = False) -> None: BasePointToSegmentPen.__init__(self) self.pen = segmentPen self.outputImpliedClosingLine = outputImpliedClosingLine @@ -271,14 +281,14 @@ class SegmentToPointPen(AbstractPen): PointPen protocol. """ - def __init__(self, pointPen, guessSmooth=True): + def __init__(self, pointPen, guessSmooth=True) -> None: if guessSmooth: self.pen = GuessSmoothPointPen(pointPen) else: self.pen = pointPen - self.contour = None + self.contour: Optional[List[Tuple[Point, SegmentType]]] = None - def _flushContour(self): + def _flushContour(self) -> None: pen = self.pen pen.beginPath() for pt, segmentType in self.contour: @@ -594,7 +604,6 @@ def addComponent(self, baseGlyphName, transformation, identifier=None, **kwargs) # if the transformation has a negative determinant, it will # reverse the contour direction of the component a, b, c, d = transformation[:4] - det = a * d - b * c if a * d - b * c < 0: pen = ReverseContourPointPen(pen) glyph.drawPoints(pen) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 8458edc359..056ad81bab 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -16,6 +16,7 @@ from fontTools.subset.svg import * from fontTools.varLib import varStore, multiVarStore # For monkey-patching from fontTools.ttLib.tables._n_a_m_e import NameRecordVisitor +from fontTools.unicodedata import mirrored import sys import struct import array @@ -2870,6 +2871,15 @@ def prune_post_subset(self, font, options): def closure_glyphs(self, s): tables = [t for t in self.tables if t.isUnicode()] + # Closure unicodes, which for now is pulling in bidi mirrored variants + if s.options.bidi_closure: + additional_unicodes = set() + for u in s.unicodes_requested: + mirror_u = mirrored(u) + if mirror_u is not None: + additional_unicodes.add(mirror_u) + s.unicodes_requested.update(additional_unicodes) + # Close glyphs for table in tables: if table.format == 14: @@ -3191,6 +3201,7 @@ def __init__(self, **kwargs): self.font_number = -1 self.pretty_svg = False self.lazy = True + self.bidi_closure = True self.set(**kwargs) diff --git a/Lib/fontTools/ttLib/tables/G_V_A_R_.py b/Lib/fontTools/ttLib/tables/G_V_A_R_.py new file mode 100644 index 0000000000..889b1f2a3b --- /dev/null +++ b/Lib/fontTools/ttLib/tables/G_V_A_R_.py @@ -0,0 +1,5 @@ +from ._g_v_a_r import table__g_v_a_r + + +class table_G_V_A_R_(table__g_v_a_r): + gid_size = 3 diff --git a/Lib/fontTools/ttLib/tables/T_S_I__0.py b/Lib/fontTools/ttLib/tables/T_S_I__0.py index 0d0e61a1cd..d60e783c60 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__0.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__0.py @@ -1,4 +1,4 @@ -""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +"""TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) tool to store its hinting source data. TSI0 is the index table containing the lengths and offsets for the glyph @@ -8,9 +8,13 @@ See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables """ -from . import DefaultTable +import logging import struct +from . import DefaultTable + +log = logging.getLogger(__name__) + tsi0Format = ">HHL" @@ -25,7 +29,14 @@ def decompile(self, data, ttFont): numGlyphs = ttFont["maxp"].numGlyphs indices = [] size = struct.calcsize(tsi0Format) - for i in range(numGlyphs + 5): + numEntries = len(data) // size + if numEntries != numGlyphs + 5: + diff = numEntries - numGlyphs - 5 + log.warning( + "Number of glyphPrograms differs from the number of glyphs in the font " + f"by {abs(diff)} ({numEntries - 5} programs vs. {numGlyphs} glyphs)." + ) + for _ in range(numEntries): glyphID, textLength, textOffset = fixlongs( *struct.unpack(tsi0Format, data[:size]) ) diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py index 24078b2256..6afd76832f 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__5.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py @@ -1,4 +1,4 @@ -""" TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) +"""TSI{0,1,2,3,5} are private tables used by Microsoft Visual TrueType (VTT) tool to store its hinting source data. TSI5 contains the VTT character groups. @@ -6,22 +6,33 @@ See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables """ +import array +import logging +import sys + from fontTools.misc.textTools import safeEval + from . import DefaultTable -import sys -import array + +log = logging.getLogger(__name__) class table_T_S_I__5(DefaultTable.DefaultTable): def decompile(self, data, ttFont): numGlyphs = ttFont["maxp"].numGlyphs - assert len(data) == 2 * numGlyphs a = array.array("H") a.frombytes(data) if sys.byteorder != "big": a.byteswap() self.glyphGrouping = {} - for i in range(numGlyphs): + numEntries = len(data) // 2 + if numEntries != numGlyphs: + diff = numEntries - numGlyphs + log.warning( + "Number of entries differs from the number of glyphs in the font " + f"by {abs(diff)} ({numEntries} entries vs. {numGlyphs} glyphs)." + ) + for i in range(numEntries): self.glyphGrouping[ttFont.getGlyphName(i)] = a[i] def compile(self, ttFont): diff --git a/Lib/fontTools/ttLib/tables/__init__.py b/Lib/fontTools/ttLib/tables/__init__.py index e622f1d134..b111097a80 100644 --- a/Lib/fontTools/ttLib/tables/__init__.py +++ b/Lib/fontTools/ttLib/tables/__init__.py @@ -23,6 +23,7 @@ def _moduleFinderHint(): from . import G_P_K_G_ from . import G_P_O_S_ from . import G_S_U_B_ + from . import G_V_A_R_ from . import G__l_a_t from . import G__l_o_c from . import H_V_A_R_ diff --git a/Lib/fontTools/ttLib/tables/_c_v_t.py b/Lib/fontTools/ttLib/tables/_c_v_t.py index 92c50a1b8d..51e2f78df8 100644 --- a/Lib/fontTools/ttLib/tables/_c_v_t.py +++ b/Lib/fontTools/ttLib/tables/_c_v_t.py @@ -21,6 +21,8 @@ def decompile(self, data, ttFont): self.values = values def compile(self, ttFont): + if not hasattr(self, "values"): + return b"" values = self.values[:] if sys.byteorder != "big": values.byteswap() diff --git a/Lib/fontTools/ttLib/tables/_f_p_g_m.py b/Lib/fontTools/ttLib/tables/_f_p_g_m.py index ba8e0488de..c21a9d4b68 100644 --- a/Lib/fontTools/ttLib/tables/_f_p_g_m.py +++ b/Lib/fontTools/ttLib/tables/_f_p_g_m.py @@ -20,7 +20,9 @@ def decompile(self, data, ttFont): self.program = program def compile(self, ttFont): - return self.program.getBytecode() + if hasattr(self, "program"): + return self.program.getBytecode() + return b"" def toXML(self, writer, ttFont): self.program.toXML(writer, ttFont) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index c05fcea5d3..ea46c9f797 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1225,7 +1225,7 @@ def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): if boundsDone is not None: boundsDone.add(glyphName) # empty components shouldn't update the bounds of the parent glyph - if g.numberOfContours == 0: + if g.yMin == g.yMax and g.xMin == g.xMax: continue x, y = compo.x, compo.y @@ -1285,11 +1285,7 @@ def getCoordinates(self, glyfTable, *, round=noRound): # however, if the referenced component glyph is another composite, we # must not round here but only at the end, after all the nested # transforms have been applied, or else rounding errors will compound. - if ( - round is not noRound - and g.numberOfContours > 0 - and not compo._hasOnlyIntegerTranslate() - ): + if round is not noRound and g.numberOfContours > 0: coordinates.toInt(round=round) if hasattr(compo, "firstPt"): # component uses two reference points: we apply the transform _before_ diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index e942beaf58..07d3befb7a 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -24,19 +24,24 @@ # FreeType2 source code for parsing 'gvar': # http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c -GVAR_HEADER_FORMAT = """ +GVAR_HEADER_FORMAT_HEAD = """ > # big endian version: H reserved: H axisCount: H sharedTupleCount: H offsetToSharedTuples: I - glyphCount: H +""" +# In between the HEAD and TAIL lies the glyphCount, which is +# of different size: 2 bytes for gvar, and 3 bytes for GVAR. +GVAR_HEADER_FORMAT_TAIL = """ + > # big endian flags: H offsetToGlyphVariationData: I """ -GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) +GVAR_HEADER_SIZE_HEAD = sstruct.calcsize(GVAR_HEADER_FORMAT_HEAD) +GVAR_HEADER_SIZE_TAIL = sstruct.calcsize(GVAR_HEADER_FORMAT_TAIL) class table__g_v_a_r(DefaultTable.DefaultTable): @@ -51,6 +56,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable): """ dependencies = ["fvar", "glyf"] + gid_size = 2 def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) @@ -74,20 +80,25 @@ def compile(self, ttFont): offsets.append(offset) compiledOffsets, tableFormat = self.compileOffsets_(offsets) + GVAR_HEADER_SIZE = GVAR_HEADER_SIZE_HEAD + self.gid_size + GVAR_HEADER_SIZE_TAIL header = {} header["version"] = self.version header["reserved"] = self.reserved header["axisCount"] = len(axisTags) header["sharedTupleCount"] = len(sharedTuples) header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets) - header["glyphCount"] = len(compiledGlyphs) header["flags"] = tableFormat header["offsetToGlyphVariationData"] = ( header["offsetToSharedTuples"] + sharedTupleSize ) - compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header) - result = [compiledHeader, compiledOffsets] + result = [ + sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), + len(compiledGlyphs).to_bytes(self.gid_size, "big"), + sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), + ] + + result.append(compiledOffsets) result.extend(sharedTuples) result.extend(compiledGlyphs) return b"".join(result) @@ -104,6 +115,7 @@ def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): pointCountUnused = 0 # pointCount is actually unused by compileGlyph result.append( compileGlyph_( + self.gid_size, variations, pointCountUnused, axisTags, @@ -116,7 +128,19 @@ def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): def decompile(self, data, ttFont): axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] glyphs = ttFont.getGlyphOrder() - sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self) + + # Parse the header + GVAR_HEADER_SIZE = GVAR_HEADER_SIZE_HEAD + self.gid_size + GVAR_HEADER_SIZE_TAIL + sstruct.unpack(GVAR_HEADER_FORMAT_HEAD, data[:GVAR_HEADER_SIZE_HEAD], self) + self.glyphCount = int.from_bytes( + data[GVAR_HEADER_SIZE_HEAD : GVAR_HEADER_SIZE_HEAD + self.gid_size], "big" + ) + sstruct.unpack( + GVAR_HEADER_FORMAT_TAIL, + data[GVAR_HEADER_SIZE_HEAD + self.gid_size : GVAR_HEADER_SIZE], + self, + ) + assert len(glyphs) == self.glyphCount assert len(axisTags) == self.axisCount sharedCoords = tv.decompileSharedTuples( @@ -146,7 +170,7 @@ def read_item(glyphName): glyph = glyf[glyphName] numPointsInGlyph = self.getNumPoints_(glyph) return decompileGlyph_( - numPointsInGlyph, sharedCoords, axisTags, gvarData + self.gid_size, numPointsInGlyph, sharedCoords, axisTags, gvarData ) return read_item @@ -264,23 +288,42 @@ def getNumPoints_(glyph): def compileGlyph_( - variations, pointCount, axisTags, sharedCoordIndices, *, optimizeSize=True + dataOffsetSize, + variations, + pointCount, + axisTags, + sharedCoordIndices, + *, + optimizeSize=True, ): + assert dataOffsetSize in (2, 3) tupleVariationCount, tuples, data = tv.compileTupleVariationStore( variations, pointCount, axisTags, sharedCoordIndices, optimizeSize=optimizeSize ) if tupleVariationCount == 0: return b"" - result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data] - if (len(tuples) + len(data)) % 2 != 0: + + offsetToData = 2 + dataOffsetSize + len(tuples) + + result = [ + tupleVariationCount.to_bytes(2, "big"), + offsetToData.to_bytes(dataOffsetSize, "big"), + tuples, + data, + ] + if (offsetToData + len(data)) % 2 != 0: result.append(b"\0") # padding return b"".join(result) -def decompileGlyph_(pointCount, sharedTuples, axisTags, data): - if len(data) < 4: +def decompileGlyph_(dataOffsetSize, pointCount, sharedTuples, axisTags, data): + assert dataOffsetSize in (2, 3) + if len(data) < 2 + dataOffsetSize: return [] - tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4]) + + tupleVariationCount = int.from_bytes(data[:2], "big") + offsetToData = int.from_bytes(data[2 : 2 + dataOffsetSize], "big") + dataPos = offsetToData return tv.decompileTupleVariationStore( "gvar", @@ -289,6 +332,6 @@ def decompileGlyph_(pointCount, sharedTuples, axisTags, data): pointCount, sharedTuples, data, - 4, + 2 + dataOffsetSize, offsetToData, ) diff --git a/Lib/fontTools/ttLib/tables/_p_o_s_t.py b/Lib/fontTools/ttLib/tables/_p_o_s_t.py index fca0812f98..c449e5f0c0 100644 --- a/Lib/fontTools/ttLib/tables/_p_o_s_t.py +++ b/Lib/fontTools/ttLib/tables/_p_o_s_t.py @@ -122,13 +122,16 @@ def build_psNameMapping(self, ttFont): glyphName = psName = self.glyphOrder[i] if glyphName == "": glyphName = "glyph%.5d" % i + if glyphName in allNames: # make up a new glyphName that's unique n = allNames[glyphName] - while (glyphName + "#" + str(n)) in allNames: + # check if the exists in any of the seen names or later ones + names = set(allNames.keys()) | set(self.glyphOrder) + while (glyphName + "." + str(n)) in names: n += 1 allNames[glyphName] = n + 1 - glyphName = glyphName + "#" + str(n) + glyphName = glyphName + "." + str(n) self.glyphOrder[i] = glyphName allNames[glyphName] = 1 diff --git a/Lib/fontTools/ttLib/tables/otBase.py b/Lib/fontTools/ttLib/tables/otBase.py index 8df7c236b1..582b02024b 100644 --- a/Lib/fontTools/ttLib/tables/otBase.py +++ b/Lib/fontTools/ttLib/tables/otBase.py @@ -398,6 +398,7 @@ def __init__(self, localState=None, tableTag=None): self.localState = localState self.tableTag = tableTag self.parent = None + self.name = "" def __setitem__(self, name, value): state = self.localState.copy() if self.localState else dict() diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py index 42c06734ea..f76938a8f1 100755 --- a/Lib/fontTools/ufoLib/__init__.py +++ b/Lib/fontTools/ufoLib/__init__.py @@ -204,7 +204,7 @@ class UFOReader(_UFOBaseIO): """Read the various components of a .ufo. Attributes: - path: An `os.PathLike` object pointing to the .ufo. + path: An :class:`os.PathLike` object pointing to the .ufo. validate: A boolean indicating if the data read should be validated. Defaults to `True`. @@ -891,7 +891,7 @@ class UFOWriter(UFOReader): """Write the various components of a .ufo. Attributes: - path: An `os.PathLike` object pointing to the .ufo. + path: An :class:`os.PathLike` object pointing to the .ufo. formatVersion: the UFO format version as a tuple of integers (major, minor), or as a single integer for the major digit only (minor is implied to be 0). By default, the latest formatVersion will be used; currently it is 3.0, diff --git a/Lib/fontTools/ufoLib/converters.py b/Lib/fontTools/ufoLib/converters.py index 88a26c616a..4ee6b05e33 100644 --- a/Lib/fontTools/ufoLib/converters.py +++ b/Lib/fontTools/ufoLib/converters.py @@ -1,11 +1,33 @@ """ -Conversion functions. +Functions for converting UFO1 or UFO2 files into UFO3 format. + +Currently provides functionality for converting kerning rules +and kerning groups. Conversion is only supported _from_ UFO1 +or UFO2, and _to_ UFO3. """ # adapted from the UFO spec def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups, glyphSet=()): + """Convert kerning data in UFO1 or UFO2 syntax into UFO3 syntax. + + Args: + kerning: + A dictionary containing the kerning rules defined in + the UFO font, as used in :class:`.UFOReader` objects. + groups: + A dictionary containing the groups defined in the UFO + font, as used in :class:`.UFOReader` objects. + glyphSet: + Optional; a set of glyph objects to skip (default: None). + + Returns: + 1. A dictionary representing the converted kerning data. + 2. A copy of the groups dictionary, with all groups renamed to UFO3 syntax. + 3. A dictionary containing the mapping of old group names to new group names. + + """ # gather known kerning groups based on the prefixes firstReferencedGroups, secondReferencedGroups = findKnownKerningGroups(groups) # Make lists of groups referenced in kerning pairs. @@ -63,35 +85,54 @@ def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups, glyphSet=()): def findKnownKerningGroups(groups): - """ - This will find kerning groups with known prefixes. - In some cases not all kerning groups will be referenced - by the kerning pairs. The algorithm for locating groups - in convertUFO1OrUFO2KerningToUFO3Kerning will miss these - unreferenced groups. By scanning for known prefixes + """Find all kerning groups in a UFO1 or UFO2 font that use known prefixes. + + In some cases, not all kerning groups will be referenced + by the kerning pairs in a UFO. The algorithm for locating + groups in :func:`convertUFO1OrUFO2KerningToUFO3Kerning` will + miss these unreferenced groups. By scanning for known prefixes, this function will catch all of the prefixed groups. - These are the prefixes and sides that are handled: + The prefixes and sides by this function are: + @MMK_L_ - side 1 @MMK_R_ - side 2 - >>> testGroups = { - ... "@MMK_L_1" : None, - ... "@MMK_L_2" : None, - ... "@MMK_L_3" : None, - ... "@MMK_R_1" : None, - ... "@MMK_R_2" : None, - ... "@MMK_R_3" : None, - ... "@MMK_l_1" : None, - ... "@MMK_r_1" : None, - ... "@MMK_X_1" : None, - ... "foo" : None, - ... } - >>> first, second = findKnownKerningGroups(testGroups) - >>> sorted(first) == ['@MMK_L_1', '@MMK_L_2', '@MMK_L_3'] - True - >>> sorted(second) == ['@MMK_R_1', '@MMK_R_2', '@MMK_R_3'] - True + as defined in the UFO1 specification. + + Args: + groups: + A dictionary containing the groups defined in the UFO + font, as read by :class:`.UFOReader`. + + Returns: + Two sets; the first containing the names of all + first-side kerning groups identified in the ``groups`` + dictionary, and the second containing the names of all + second-side kerning groups identified. + + "First-side" and "second-side" are with respect to the + writing direction of the script. + + Example:: + + >>> testGroups = { + ... "@MMK_L_1" : None, + ... "@MMK_L_2" : None, + ... "@MMK_L_3" : None, + ... "@MMK_R_1" : None, + ... "@MMK_R_2" : None, + ... "@MMK_R_3" : None, + ... "@MMK_l_1" : None, + ... "@MMK_r_1" : None, + ... "@MMK_X_1" : None, + ... "foo" : None, + ... } + >>> first, second = findKnownKerningGroups(testGroups) + >>> sorted(first) == ['@MMK_L_1', '@MMK_L_2', '@MMK_L_3'] + True + >>> sorted(second) == ['@MMK_R_1', '@MMK_R_2', '@MMK_R_3'] + True """ knownFirstGroupPrefixes = ["@MMK_L_"] knownSecondGroupPrefixes = ["@MMK_R_"] @@ -110,6 +151,27 @@ def findKnownKerningGroups(groups): def makeUniqueGroupName(name, groupNames, counter=0): + """Make a kerning group name that will be unique within the set of group names. + + If the requested kerning group name already exists within the set, this + will return a new name by adding an incremented counter to the end + of the requested name. + + Args: + name: + The requested kerning group name. + groupNames: + A list of the existing kerning group names. + counter: + Optional; a counter of group names already seen (default: 0). If + :attr:`.counter` is not provided, the function will recurse, + incrementing the value of :attr:`.counter` until it finds the + first unused ``name+counter`` combination, and return that result. + + Returns: + A unique kerning group name composed of the requested name suffixed + by the smallest available integer counter. + """ # Add a number to the name if the counter is higher than zero. newName = name if counter > 0: @@ -123,6 +185,8 @@ def makeUniqueGroupName(name, groupNames, counter=0): def test(): """ + Tests for :func:`.convertUFO1OrUFO2KerningToUFO3Kerning`. + No known prefixes. >>> testKerning = { diff --git a/Lib/fontTools/ufoLib/errors.py b/Lib/fontTools/ufoLib/errors.py index e05dd438b4..6cc9fec399 100644 --- a/Lib/fontTools/ufoLib/errors.py +++ b/Lib/fontTools/ufoLib/errors.py @@ -10,6 +10,14 @@ class UnsupportedUFOFormat(UFOLibError): class GlifLibError(UFOLibError): + """An error raised by glifLib. + + This class is a loose backport of PEP 678, adding a :attr:`.note` + attribute that can hold additional context for errors encountered. + + It will be maintained until only Python 3.11-and-later are supported. + """ + def _add_note(self, note: str) -> None: # Loose backport of PEP 678 until we only support Python 3.11+, used for # adding additional context to errors. diff --git a/Lib/fontTools/ufoLib/etree.py b/Lib/fontTools/ufoLib/etree.py index 77e3c16e2b..07b924ac85 100644 --- a/Lib/fontTools/ufoLib/etree.py +++ b/Lib/fontTools/ufoLib/etree.py @@ -1,5 +1,5 @@ """DEPRECATED - This module is kept here only as a backward compatibility shim -for the old ufoLib.etree module, which was moved to fontTools.misc.etree. +for the old ufoLib.etree module, which was moved to :mod:`fontTools.misc.etree`. Please use the latter instead. """ diff --git a/Lib/fontTools/ufoLib/filenames.py b/Lib/fontTools/ufoLib/filenames.py index 7f1af58ee8..83442f1c8c 100644 --- a/Lib/fontTools/ufoLib/filenames.py +++ b/Lib/fontTools/ufoLib/filenames.py @@ -1,6 +1,22 @@ """ -User name to file name conversion. -This was taken from the UFO 3 spec. +Convert user-provided internal UFO names to spec-compliant filenames. + +This module implements the algorithm for converting between a "user name" - +something that a user can choose arbitrarily inside a font editor - and a file +name suitable for use in a wide range of operating systems and filesystems. + +The `UFO 3 specification `_ +provides an example of an algorithm for such conversion, which avoids illegal +characters, reserved file names, ambiguity between upper- and lower-case +characters, and clashes with existing files. + +This code was originally copied from +`ufoLib `_ +by Tal Leming and is copyright (c) 2005-2016, The RoboFab Developers: + +- Erik van Blokland +- Tal Leming +- Just van Rossum """ # Restrictions are taken mostly from @@ -93,53 +109,69 @@ class NameTranslationError(Exception): def userNameToFileName(userName: str, existing=(), prefix="", suffix=""): - """ - `existing` should be a set-like object. - - >>> userNameToFileName("a") == "a" - True - >>> userNameToFileName("A") == "A_" - True - >>> userNameToFileName("AE") == "A_E_" - True - >>> userNameToFileName("Ae") == "A_e" - True - >>> userNameToFileName("ae") == "ae" - True - >>> userNameToFileName("aE") == "aE_" - True - >>> userNameToFileName("a.alt") == "a.alt" - True - >>> userNameToFileName("A.alt") == "A_.alt" - True - >>> userNameToFileName("A.Alt") == "A_.A_lt" - True - >>> userNameToFileName("A.aLt") == "A_.aL_t" - True - >>> userNameToFileName(u"A.alT") == "A_.alT_" - True - >>> userNameToFileName("T_H") == "T__H_" - True - >>> userNameToFileName("T_h") == "T__h" - True - >>> userNameToFileName("t_h") == "t_h" - True - >>> userNameToFileName("F_F_I") == "F__F__I_" - True - >>> userNameToFileName("f_f_i") == "f_f_i" - True - >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" - True - >>> userNameToFileName(".notdef") == "_notdef" - True - >>> userNameToFileName("con") == "_con" - True - >>> userNameToFileName("CON") == "C_O_N_" - True - >>> userNameToFileName("con.alt") == "_con.alt" - True - >>> userNameToFileName("alt.con") == "alt._con" - True + """Converts from a user name to a file name. + + Takes care to avoid illegal characters, reserved file names, ambiguity between + upper- and lower-case characters, and clashes with existing files. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + Raises: + NameTranslationError: If no suitable name could be generated. + + Examples:: + + >>> userNameToFileName("a") == "a" + True + >>> userNameToFileName("A") == "A_" + True + >>> userNameToFileName("AE") == "A_E_" + True + >>> userNameToFileName("Ae") == "A_e" + True + >>> userNameToFileName("ae") == "ae" + True + >>> userNameToFileName("aE") == "aE_" + True + >>> userNameToFileName("a.alt") == "a.alt" + True + >>> userNameToFileName("A.alt") == "A_.alt" + True + >>> userNameToFileName("A.Alt") == "A_.A_lt" + True + >>> userNameToFileName("A.aLt") == "A_.aL_t" + True + >>> userNameToFileName(u"A.alT") == "A_.alT_" + True + >>> userNameToFileName("T_H") == "T__H_" + True + >>> userNameToFileName("T_h") == "T__h" + True + >>> userNameToFileName("t_h") == "t_h" + True + >>> userNameToFileName("F_F_I") == "F__F__I_" + True + >>> userNameToFileName("f_f_i") == "f_f_i" + True + >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" + True + >>> userNameToFileName(".notdef") == "_notdef" + True + >>> userNameToFileName("con") == "_con" + True + >>> userNameToFileName("CON") == "C_O_N_" + True + >>> userNameToFileName("con.alt") == "_con.alt" + True + >>> userNameToFileName("alt.con") == "alt._con" + True """ # the incoming name must be a string if not isinstance(userName, str): @@ -181,33 +213,42 @@ def userNameToFileName(userName: str, existing=(), prefix="", suffix=""): def handleClash1(userName, existing=[], prefix="", suffix=""): - """ - existing should be a case-insensitive list - of all existing file names. - - >>> prefix = ("0" * 5) + "." - >>> suffix = "." + ("0" * 10) - >>> existing = ["a" * 5] - - >>> e = list(existing) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000001.0000000000') - True - - >>> e = list(existing) - >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000002.0000000000') - True - - >>> e = list(existing) - >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000001.0000000000') - True + """A helper function that resolves collisions with existing names when choosing a filename. + + This function attempts to append an unused integer counter to the filename. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = ["a" * 5] + + >>> e = list(existing) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000002.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True """ # if the prefix length + user name length + suffix length + 15 is at # or past the maximum length, silce 15 characters off of the user name @@ -238,30 +279,44 @@ def handleClash1(userName, existing=[], prefix="", suffix=""): def handleClash2(existing=[], prefix="", suffix=""): - """ - existing should be a case-insensitive list - of all existing file names. - - >>> prefix = ("0" * 5) + "." - >>> suffix = "." + ("0" * 10) - >>> existing = [prefix + str(i) + suffix for i in range(100)] - - >>> e = list(existing) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.100.0000000000') - True - - >>> e = list(existing) - >>> e.remove(prefix + "1" + suffix) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.1.0000000000') - True - - >>> e = list(existing) - >>> e.remove(prefix + "2" + suffix) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.2.0000000000') - True + """A helper function that resolves collisions with existing names when choosing a filename. + + This function is a fallback to :func:`handleClash1`. It attempts to append an unused integer counter to the filename. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + Raises: + NameTranslationError: If no suitable name could be generated. + + Examples:: + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = [prefix + str(i) + suffix for i in range(100)] + + >>> e = list(existing) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.100.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "1" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.1.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "2" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.2.0000000000') + True """ # calculate the longest possible string maxLength = maxFileNameLength - len(prefix) - len(suffix) diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py index abbda49146..a5a05003ee 100755 --- a/Lib/fontTools/ufoLib/glifLib.py +++ b/Lib/fontTools/ufoLib/glifLib.py @@ -1,11 +1,11 @@ """ -glifLib.py -- Generic module for reading and writing the .glif format. +Generic module for reading and writing the .glif format. More info about the .glif format (GLyphInterchangeFormat) can be found here: http://unifiedfontobject.org -The main class in this module is GlyphSet. It manages a set of .glif files +The main class in this module is :class:`GlyphSet`. It manages a set of .glif files in a folder. It offers two ways to read glyph data, and one way to write glyph data. See the class doc string for details. """ @@ -60,6 +60,13 @@ class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum): + """Class representing the versions of the .glif format supported by the UFO version in use. + + For a given :mod:`fontTools.ufoLib.UFOFormatVersion`, the :func:`supported_versions` method will + return the supported versions of the GLIF file format. If the UFO version is unspecified, the + :func:`supported_versions` method will return all available GLIF format versions. + """ + FORMAT_1_0 = (1, 0) FORMAT_2_0 = (2, 0) diff --git a/Lib/fontTools/ufoLib/kerning.py b/Lib/fontTools/ufoLib/kerning.py index 8a1dca5b68..5c84dd720a 100644 --- a/Lib/fontTools/ufoLib/kerning.py +++ b/Lib/fontTools/ufoLib/kerning.py @@ -1,43 +1,73 @@ def lookupKerningValue( pair, kerning, groups, fallback=0, glyphToFirstGroup=None, glyphToSecondGroup=None ): - """ - Note: This expects kerning to be a flat dictionary - of kerning pairs, not the nested structure used - in kerning.plist. + """Retrieve the kerning value (if any) between a pair of elements. + + The elments can be either individual glyphs (by name) or kerning + groups (by name), or any combination of the two. + + Args: + pair: + A tuple, in logical order (first, second) with respect + to the reading direction, to query the font for kerning + information on. Each element in the tuple can be either + a glyph name or a kerning group name. + kerning: + A dictionary of kerning pairs. + groups: + A set of kerning groups. + fallback: + The fallback value to return if no kern is found between + the elements in ``pair``. Defaults to 0. + glyphToFirstGroup: + A dictionary mapping glyph names to the first-glyph kerning + groups to which they belong. Defaults to ``None``. + glyphToSecondGroup: + A dictionary mapping glyph names to the second-glyph kerning + groups to which they belong. Defaults to ``None``. + + Returns: + The kerning value between the element pair. If no kerning for + the pair is found, the fallback value is returned. + + Note: This function expects the ``kerning`` argument to be a flat + dictionary of kerning pairs, not the nested structure used in a + kerning.plist file. + + Examples:: - >>> groups = { - ... "public.kern1.O" : ["O", "D", "Q"], - ... "public.kern2.E" : ["E", "F"] - ... } - >>> kerning = { - ... ("public.kern1.O", "public.kern2.E") : -100, - ... ("public.kern1.O", "F") : -200, - ... ("D", "F") : -300 - ... } - >>> lookupKerningValue(("D", "F"), kerning, groups) - -300 - >>> lookupKerningValue(("O", "F"), kerning, groups) - -200 - >>> lookupKerningValue(("O", "E"), kerning, groups) - -100 - >>> lookupKerningValue(("O", "O"), kerning, groups) - 0 - >>> lookupKerningValue(("E", "E"), kerning, groups) - 0 - >>> lookupKerningValue(("E", "O"), kerning, groups) - 0 - >>> lookupKerningValue(("X", "X"), kerning, groups) - 0 - >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"), - ... kerning, groups) - -100 - >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups) - -200 - >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups) - -100 - >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups) - 0 + >>> groups = { + ... "public.kern1.O" : ["O", "D", "Q"], + ... "public.kern2.E" : ["E", "F"] + ... } + >>> kerning = { + ... ("public.kern1.O", "public.kern2.E") : -100, + ... ("public.kern1.O", "F") : -200, + ... ("D", "F") : -300 + ... } + >>> lookupKerningValue(("D", "F"), kerning, groups) + -300 + >>> lookupKerningValue(("O", "F"), kerning, groups) + -200 + >>> lookupKerningValue(("O", "E"), kerning, groups) + -100 + >>> lookupKerningValue(("O", "O"), kerning, groups) + 0 + >>> lookupKerningValue(("E", "E"), kerning, groups) + 0 + >>> lookupKerningValue(("E", "O"), kerning, groups) + 0 + >>> lookupKerningValue(("X", "X"), kerning, groups) + 0 + >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"), + ... kerning, groups) + -100 + >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups) + -200 + >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups) + -100 + >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups) + 0 """ # quickly check to see if the pair is in the kerning dictionary if pair in kerning: diff --git a/Lib/fontTools/ufoLib/utils.py b/Lib/fontTools/ufoLib/utils.py index 45ec1c564b..45ae5e812e 100644 --- a/Lib/fontTools/ufoLib/utils.py +++ b/Lib/fontTools/ufoLib/utils.py @@ -1,5 +1,8 @@ -"""The module contains miscellaneous helpers. -It's not considered part of the public ufoLib API. +"""This module contains miscellaneous helpers. + +It is not considered part of the public ufoLib API. It does, however, +define the :py:obj:`.deprecated` decorator that is used elsewhere in +the module. """ import warnings diff --git a/Lib/fontTools/unicodedata/Mirrored.py b/Lib/fontTools/unicodedata/Mirrored.py new file mode 100644 index 0000000000..75b51a9012 --- /dev/null +++ b/Lib/fontTools/unicodedata/Mirrored.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +# +# NOTE: The mappings in this file were generated from the command line: +# cat BidiMirroring.txt | grep "^[0-9A-F]" | sed "s/;//" | awk '{print " 0x"$1": 0x"$2","}' +# +# Source: http://www.unicode.org/Public/UNIDATA/BidiMirroring.txt +# License: http://unicode.org/copyright.html#License +# +# BidiMirroring-16.0.0.txt +# Date: 2024-01-30 +# © 2024 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use and license, see https://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see https://www.unicode.org/reports/tr44/ +MIRRORED = { + 0x0028: 0x0029, + 0x0029: 0x0028, + 0x003C: 0x003E, + 0x003E: 0x003C, + 0x005B: 0x005D, + 0x005D: 0x005B, + 0x007B: 0x007D, + 0x007D: 0x007B, + 0x00AB: 0x00BB, + 0x00BB: 0x00AB, + 0x0F3A: 0x0F3B, + 0x0F3B: 0x0F3A, + 0x0F3C: 0x0F3D, + 0x0F3D: 0x0F3C, + 0x169B: 0x169C, + 0x169C: 0x169B, + 0x2039: 0x203A, + 0x203A: 0x2039, + 0x2045: 0x2046, + 0x2046: 0x2045, + 0x207D: 0x207E, + 0x207E: 0x207D, + 0x208D: 0x208E, + 0x208E: 0x208D, + 0x2208: 0x220B, + 0x2209: 0x220C, + 0x220A: 0x220D, + 0x220B: 0x2208, + 0x220C: 0x2209, + 0x220D: 0x220A, + 0x2215: 0x29F5, + 0x221F: 0x2BFE, + 0x2220: 0x29A3, + 0x2221: 0x299B, + 0x2222: 0x29A0, + 0x2224: 0x2AEE, + 0x223C: 0x223D, + 0x223D: 0x223C, + 0x2243: 0x22CD, + 0x2245: 0x224C, + 0x224C: 0x2245, + 0x2252: 0x2253, + 0x2253: 0x2252, + 0x2254: 0x2255, + 0x2255: 0x2254, + 0x2264: 0x2265, + 0x2265: 0x2264, + 0x2266: 0x2267, + 0x2267: 0x2266, + 0x2268: 0x2269, + 0x2269: 0x2268, + 0x226A: 0x226B, + 0x226B: 0x226A, + 0x226E: 0x226F, + 0x226F: 0x226E, + 0x2270: 0x2271, + 0x2271: 0x2270, + 0x2272: 0x2273, + 0x2273: 0x2272, + 0x2274: 0x2275, + 0x2275: 0x2274, + 0x2276: 0x2277, + 0x2277: 0x2276, + 0x2278: 0x2279, + 0x2279: 0x2278, + 0x227A: 0x227B, + 0x227B: 0x227A, + 0x227C: 0x227D, + 0x227D: 0x227C, + 0x227E: 0x227F, + 0x227F: 0x227E, + 0x2280: 0x2281, + 0x2281: 0x2280, + 0x2282: 0x2283, + 0x2283: 0x2282, + 0x2284: 0x2285, + 0x2285: 0x2284, + 0x2286: 0x2287, + 0x2287: 0x2286, + 0x2288: 0x2289, + 0x2289: 0x2288, + 0x228A: 0x228B, + 0x228B: 0x228A, + 0x228F: 0x2290, + 0x2290: 0x228F, + 0x2291: 0x2292, + 0x2292: 0x2291, + 0x2298: 0x29B8, + 0x22A2: 0x22A3, + 0x22A3: 0x22A2, + 0x22A6: 0x2ADE, + 0x22A8: 0x2AE4, + 0x22A9: 0x2AE3, + 0x22AB: 0x2AE5, + 0x22B0: 0x22B1, + 0x22B1: 0x22B0, + 0x22B2: 0x22B3, + 0x22B3: 0x22B2, + 0x22B4: 0x22B5, + 0x22B5: 0x22B4, + 0x22B6: 0x22B7, + 0x22B7: 0x22B6, + 0x22B8: 0x27DC, + 0x22C9: 0x22CA, + 0x22CA: 0x22C9, + 0x22CB: 0x22CC, + 0x22CC: 0x22CB, + 0x22CD: 0x2243, + 0x22D0: 0x22D1, + 0x22D1: 0x22D0, + 0x22D6: 0x22D7, + 0x22D7: 0x22D6, + 0x22D8: 0x22D9, + 0x22D9: 0x22D8, + 0x22DA: 0x22DB, + 0x22DB: 0x22DA, + 0x22DC: 0x22DD, + 0x22DD: 0x22DC, + 0x22DE: 0x22DF, + 0x22DF: 0x22DE, + 0x22E0: 0x22E1, + 0x22E1: 0x22E0, + 0x22E2: 0x22E3, + 0x22E3: 0x22E2, + 0x22E4: 0x22E5, + 0x22E5: 0x22E4, + 0x22E6: 0x22E7, + 0x22E7: 0x22E6, + 0x22E8: 0x22E9, + 0x22E9: 0x22E8, + 0x22EA: 0x22EB, + 0x22EB: 0x22EA, + 0x22EC: 0x22ED, + 0x22ED: 0x22EC, + 0x22F0: 0x22F1, + 0x22F1: 0x22F0, + 0x22F2: 0x22FA, + 0x22F3: 0x22FB, + 0x22F4: 0x22FC, + 0x22F6: 0x22FD, + 0x22F7: 0x22FE, + 0x22FA: 0x22F2, + 0x22FB: 0x22F3, + 0x22FC: 0x22F4, + 0x22FD: 0x22F6, + 0x22FE: 0x22F7, + 0x2308: 0x2309, + 0x2309: 0x2308, + 0x230A: 0x230B, + 0x230B: 0x230A, + 0x2329: 0x232A, + 0x232A: 0x2329, + 0x2768: 0x2769, + 0x2769: 0x2768, + 0x276A: 0x276B, + 0x276B: 0x276A, + 0x276C: 0x276D, + 0x276D: 0x276C, + 0x276E: 0x276F, + 0x276F: 0x276E, + 0x2770: 0x2771, + 0x2771: 0x2770, + 0x2772: 0x2773, + 0x2773: 0x2772, + 0x2774: 0x2775, + 0x2775: 0x2774, + 0x27C3: 0x27C4, + 0x27C4: 0x27C3, + 0x27C5: 0x27C6, + 0x27C6: 0x27C5, + 0x27C8: 0x27C9, + 0x27C9: 0x27C8, + 0x27CB: 0x27CD, + 0x27CD: 0x27CB, + 0x27D5: 0x27D6, + 0x27D6: 0x27D5, + 0x27DC: 0x22B8, + 0x27DD: 0x27DE, + 0x27DE: 0x27DD, + 0x27E2: 0x27E3, + 0x27E3: 0x27E2, + 0x27E4: 0x27E5, + 0x27E5: 0x27E4, + 0x27E6: 0x27E7, + 0x27E7: 0x27E6, + 0x27E8: 0x27E9, + 0x27E9: 0x27E8, + 0x27EA: 0x27EB, + 0x27EB: 0x27EA, + 0x27EC: 0x27ED, + 0x27ED: 0x27EC, + 0x27EE: 0x27EF, + 0x27EF: 0x27EE, + 0x2983: 0x2984, + 0x2984: 0x2983, + 0x2985: 0x2986, + 0x2986: 0x2985, + 0x2987: 0x2988, + 0x2988: 0x2987, + 0x2989: 0x298A, + 0x298A: 0x2989, + 0x298B: 0x298C, + 0x298C: 0x298B, + 0x298D: 0x2990, + 0x298E: 0x298F, + 0x298F: 0x298E, + 0x2990: 0x298D, + 0x2991: 0x2992, + 0x2992: 0x2991, + 0x2993: 0x2994, + 0x2994: 0x2993, + 0x2995: 0x2996, + 0x2996: 0x2995, + 0x2997: 0x2998, + 0x2998: 0x2997, + 0x299B: 0x2221, + 0x29A0: 0x2222, + 0x29A3: 0x2220, + 0x29A4: 0x29A5, + 0x29A5: 0x29A4, + 0x29A8: 0x29A9, + 0x29A9: 0x29A8, + 0x29AA: 0x29AB, + 0x29AB: 0x29AA, + 0x29AC: 0x29AD, + 0x29AD: 0x29AC, + 0x29AE: 0x29AF, + 0x29AF: 0x29AE, + 0x29B8: 0x2298, + 0x29C0: 0x29C1, + 0x29C1: 0x29C0, + 0x29C4: 0x29C5, + 0x29C5: 0x29C4, + 0x29CF: 0x29D0, + 0x29D0: 0x29CF, + 0x29D1: 0x29D2, + 0x29D2: 0x29D1, + 0x29D4: 0x29D5, + 0x29D5: 0x29D4, + 0x29D8: 0x29D9, + 0x29D9: 0x29D8, + 0x29DA: 0x29DB, + 0x29DB: 0x29DA, + 0x29E8: 0x29E9, + 0x29E9: 0x29E8, + 0x29F5: 0x2215, + 0x29F8: 0x29F9, + 0x29F9: 0x29F8, + 0x29FC: 0x29FD, + 0x29FD: 0x29FC, + 0x2A2B: 0x2A2C, + 0x2A2C: 0x2A2B, + 0x2A2D: 0x2A2E, + 0x2A2E: 0x2A2D, + 0x2A34: 0x2A35, + 0x2A35: 0x2A34, + 0x2A3C: 0x2A3D, + 0x2A3D: 0x2A3C, + 0x2A64: 0x2A65, + 0x2A65: 0x2A64, + 0x2A79: 0x2A7A, + 0x2A7A: 0x2A79, + 0x2A7B: 0x2A7C, + 0x2A7C: 0x2A7B, + 0x2A7D: 0x2A7E, + 0x2A7E: 0x2A7D, + 0x2A7F: 0x2A80, + 0x2A80: 0x2A7F, + 0x2A81: 0x2A82, + 0x2A82: 0x2A81, + 0x2A83: 0x2A84, + 0x2A84: 0x2A83, + 0x2A85: 0x2A86, + 0x2A86: 0x2A85, + 0x2A87: 0x2A88, + 0x2A88: 0x2A87, + 0x2A89: 0x2A8A, + 0x2A8A: 0x2A89, + 0x2A8B: 0x2A8C, + 0x2A8C: 0x2A8B, + 0x2A8D: 0x2A8E, + 0x2A8E: 0x2A8D, + 0x2A8F: 0x2A90, + 0x2A90: 0x2A8F, + 0x2A91: 0x2A92, + 0x2A92: 0x2A91, + 0x2A93: 0x2A94, + 0x2A94: 0x2A93, + 0x2A95: 0x2A96, + 0x2A96: 0x2A95, + 0x2A97: 0x2A98, + 0x2A98: 0x2A97, + 0x2A99: 0x2A9A, + 0x2A9A: 0x2A99, + 0x2A9B: 0x2A9C, + 0x2A9C: 0x2A9B, + 0x2A9D: 0x2A9E, + 0x2A9E: 0x2A9D, + 0x2A9F: 0x2AA0, + 0x2AA0: 0x2A9F, + 0x2AA1: 0x2AA2, + 0x2AA2: 0x2AA1, + 0x2AA6: 0x2AA7, + 0x2AA7: 0x2AA6, + 0x2AA8: 0x2AA9, + 0x2AA9: 0x2AA8, + 0x2AAA: 0x2AAB, + 0x2AAB: 0x2AAA, + 0x2AAC: 0x2AAD, + 0x2AAD: 0x2AAC, + 0x2AAF: 0x2AB0, + 0x2AB0: 0x2AAF, + 0x2AB1: 0x2AB2, + 0x2AB2: 0x2AB1, + 0x2AB3: 0x2AB4, + 0x2AB4: 0x2AB3, + 0x2AB5: 0x2AB6, + 0x2AB6: 0x2AB5, + 0x2AB7: 0x2AB8, + 0x2AB8: 0x2AB7, + 0x2AB9: 0x2ABA, + 0x2ABA: 0x2AB9, + 0x2ABB: 0x2ABC, + 0x2ABC: 0x2ABB, + 0x2ABD: 0x2ABE, + 0x2ABE: 0x2ABD, + 0x2ABF: 0x2AC0, + 0x2AC0: 0x2ABF, + 0x2AC1: 0x2AC2, + 0x2AC2: 0x2AC1, + 0x2AC3: 0x2AC4, + 0x2AC4: 0x2AC3, + 0x2AC5: 0x2AC6, + 0x2AC6: 0x2AC5, + 0x2AC7: 0x2AC8, + 0x2AC8: 0x2AC7, + 0x2AC9: 0x2ACA, + 0x2ACA: 0x2AC9, + 0x2ACB: 0x2ACC, + 0x2ACC: 0x2ACB, + 0x2ACD: 0x2ACE, + 0x2ACE: 0x2ACD, + 0x2ACF: 0x2AD0, + 0x2AD0: 0x2ACF, + 0x2AD1: 0x2AD2, + 0x2AD2: 0x2AD1, + 0x2AD3: 0x2AD4, + 0x2AD4: 0x2AD3, + 0x2AD5: 0x2AD6, + 0x2AD6: 0x2AD5, + 0x2ADE: 0x22A6, + 0x2AE3: 0x22A9, + 0x2AE4: 0x22A8, + 0x2AE5: 0x22AB, + 0x2AEC: 0x2AED, + 0x2AED: 0x2AEC, + 0x2AEE: 0x2224, + 0x2AF7: 0x2AF8, + 0x2AF8: 0x2AF7, + 0x2AF9: 0x2AFA, + 0x2AFA: 0x2AF9, + 0x2BFE: 0x221F, + 0x2E02: 0x2E03, + 0x2E03: 0x2E02, + 0x2E04: 0x2E05, + 0x2E05: 0x2E04, + 0x2E09: 0x2E0A, + 0x2E0A: 0x2E09, + 0x2E0C: 0x2E0D, + 0x2E0D: 0x2E0C, + 0x2E1C: 0x2E1D, + 0x2E1D: 0x2E1C, + 0x2E20: 0x2E21, + 0x2E21: 0x2E20, + 0x2E22: 0x2E23, + 0x2E23: 0x2E22, + 0x2E24: 0x2E25, + 0x2E25: 0x2E24, + 0x2E26: 0x2E27, + 0x2E27: 0x2E26, + 0x2E28: 0x2E29, + 0x2E29: 0x2E28, + 0x2E55: 0x2E56, + 0x2E56: 0x2E55, + 0x2E57: 0x2E58, + 0x2E58: 0x2E57, + 0x2E59: 0x2E5A, + 0x2E5A: 0x2E59, + 0x2E5B: 0x2E5C, + 0x2E5C: 0x2E5B, + 0x3008: 0x3009, + 0x3009: 0x3008, + 0x300A: 0x300B, + 0x300B: 0x300A, + 0x300C: 0x300D, + 0x300D: 0x300C, + 0x300E: 0x300F, + 0x300F: 0x300E, + 0x3010: 0x3011, + 0x3011: 0x3010, + 0x3014: 0x3015, + 0x3015: 0x3014, + 0x3016: 0x3017, + 0x3017: 0x3016, + 0x3018: 0x3019, + 0x3019: 0x3018, + 0x301A: 0x301B, + 0x301B: 0x301A, + 0xFE59: 0xFE5A, + 0xFE5A: 0xFE59, + 0xFE5B: 0xFE5C, + 0xFE5C: 0xFE5B, + 0xFE5D: 0xFE5E, + 0xFE5E: 0xFE5D, + 0xFE64: 0xFE65, + 0xFE65: 0xFE64, + 0xFF08: 0xFF09, + 0xFF09: 0xFF08, + 0xFF1C: 0xFF1E, + 0xFF1E: 0xFF1C, + 0xFF3B: 0xFF3D, + 0xFF3D: 0xFF3B, + 0xFF5B: 0xFF5D, + 0xFF5D: 0xFF5B, + 0xFF5F: 0xFF60, + 0xFF60: 0xFF5F, + 0xFF62: 0xFF63, + 0xFF63: 0xFF62, +} diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py index edae44ec71..1adb07d289 100644 --- a/Lib/fontTools/unicodedata/__init__.py +++ b/Lib/fontTools/unicodedata/__init__.py @@ -15,8 +15,7 @@ # fall back to built-in unicodedata (possibly outdated) from unicodedata import * -from . import Blocks, Scripts, ScriptExtensions, OTTags - +from . import Blocks, Mirrored, Scripts, ScriptExtensions, OTTags __all__ = [ # names from built-in unicodedata module @@ -46,6 +45,11 @@ ] +def mirrored(code): + """If code (unicode codepoint) has a mirrored version returns it, otherwise None.""" + return Mirrored.MIRRORED.get(code) + + def script(char): """Return the four-letter script code assigned to the Unicode character 'char' as string. diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index f3d8d59dc5..e3c00d73fa 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -555,6 +555,8 @@ def _add_VHVAR(font, axisTags, tableFields, getAdvanceMetrics): varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0], round=noRound) varData.optimize() directStore = builder.buildVarStore(varTupleList, [varData]) + # remove unused regions from VarRegionList + directStore.prune_regions() # Build optimized indirect mapping storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) diff --git a/Lib/fontTools/voltLib/__main__.py b/Lib/fontTools/voltLib/__main__.py new file mode 100644 index 0000000000..aa2c3b3e61 --- /dev/null +++ b/Lib/fontTools/voltLib/__main__.py @@ -0,0 +1,206 @@ +import argparse +import logging +import sys +from io import StringIO +from pathlib import Path + +from fontTools import configLogger +from fontTools.feaLib.builder import addOpenTypeFeaturesFromString +from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.lexer import Lexer +from fontTools.misc.cliTools import makeOutputFileName +from fontTools.ttLib import TTFont, TTLibError +from fontTools.voltLib.parser import Parser +from fontTools.voltLib.voltToFea import TABLES, VoltToFea + +log = logging.getLogger("fontTools.feaLib") + +SUPPORTED_TABLES = TABLES + ["cmap"] + + +def invalid_fea_glyph_name(name): + """Check if the glyph name is valid according to FEA syntax.""" + if name[0] not in Lexer.CHAR_NAME_START_: + return True + if any(c not in Lexer.CHAR_NAME_CONTINUATION_ for c in name[1:]): + return True + return False + + +def sanitize_glyph_name(name): + """Sanitize the glyph name to ensure it is valid according to FEA syntax.""" + sanitized = "" + for i, c in enumerate(name): + if i == 0 and c not in Lexer.CHAR_NAME_START_: + sanitized += "a" + c + elif c not in Lexer.CHAR_NAME_CONTINUATION_: + sanitized += "_" + else: + sanitized += c + + return sanitized + + +def main(args=None): + """Build tables from a MS VOLT project into an OTF font""" + parser = argparse.ArgumentParser( + description="Use fontTools to compile MS VOLT projects." + ) + parser.add_argument( + "input", + metavar="INPUT", + help="Path to the input font/VTP file to process", + type=Path, + ) + parser.add_argument( + "-f", + "--font", + metavar="INPUT_FONT", + help="Path to the input font (if INPUT is a VTP file)", + type=Path, + ) + parser.add_argument( + "-o", + "--output", + dest="output", + metavar="OUTPUT", + help="Path to the output font.", + type=Path, + ) + parser.add_argument( + "-t", + "--tables", + metavar="TABLE_TAG", + choices=SUPPORTED_TABLES, + nargs="+", + help="Specify the table(s) to be built.", + ) + parser.add_argument( + "-F", + "--debug-feature-file", + help="Write the generated feature file to disk.", + action="store_true", + ) + parser.add_argument( + "--ship", + help="Remove source VOLT tables from output font.", + action="store_true", + ) + parser.add_argument( + "-v", + "--verbose", + help="Increase the logger verbosity. Multiple -v options are allowed.", + action="count", + default=0, + ) + parser.add_argument( + "-T", + "--traceback", + help="show traceback for exceptions.", + action="store_true", + ) + options = parser.parse_args(args) + + levels = ["WARNING", "INFO", "DEBUG"] + configLogger(level=levels[min(len(levels) - 1, options.verbose)]) + + output_font = options.output or Path( + makeOutputFileName(options.font or options.input) + ) + log.info(f"Compiling MS VOLT to '{output_font}'") + + file_or_path = options.input + font = None + + # If the input is a font file, extract the VOLT data from the "TSIV" table + try: + font = TTFont(file_or_path) + if "TSIV" in font: + file_or_path = StringIO(font["TSIV"].data.decode("utf-8")) + else: + log.error('"TSIV" table is missing') + return 1 + except TTLibError: + pass + + # If input is not a font file, the font must be provided + if font is None: + if not options.font: + log.error("Please provide an input font") + return 1 + font = TTFont(options.font) + + # FEA syntax does not allow some glyph names that VOLT accepts, so if we + # found such glyph name we will temporarily rename such glyphs. + glyphOrder = font.getGlyphOrder() + tempGlyphOrder = None + if any(invalid_fea_glyph_name(n) for n in glyphOrder): + tempGlyphOrder = [] + for n in glyphOrder: + if invalid_fea_glyph_name(n): + n = sanitize_glyph_name(n) + existing = set(tempGlyphOrder) | set(glyphOrder) + while n in existing: + n = "a" + n + tempGlyphOrder.append(n) + font.setGlyphOrder(tempGlyphOrder) + + doc = Parser(file_or_path).parse() + + log.info("Converting VTP data to FEA") + converter = VoltToFea(doc, font) + try: + fea = converter.convert(options.tables, ignore_unsupported_settings=True) + except NotImplementedError as e: + if options.traceback: + raise + location = getattr(e.args[0], "location", None) + message = f'"{e}" is not supported' + if location: + path, line, column = location + log.error(f"{path}:{line}:{column}: {message}") + else: + log.error(message) + return 1 + + fea_filename = options.input + if options.debug_feature_file: + fea_filename = output_font.with_suffix(".fea") + log.info(f"Writing FEA to '{fea_filename}'") + with open(fea_filename, "w") as fp: + fp.write(fea) + + log.info("Compiling FEA to OpenType tables") + try: + addOpenTypeFeaturesFromString( + font, + fea, + filename=fea_filename, + tables=options.tables, + ) + except FeatureLibError as e: + if options.traceback: + raise + log.error(e) + return 1 + + if options.ship: + for tag in ["TSIV", "TSIS", "TSIP", "TSID"]: + if tag in font: + del font[tag] + + # Restore original glyph names. + if tempGlyphOrder: + import io + + f = io.BytesIO() + font.save(f) + font = TTFont(f) + font.setGlyphOrder(glyphOrder) + font["post"].extraNames = [] + + font.save(output_font) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Lib/fontTools/voltLib/ast.py b/Lib/fontTools/voltLib/ast.py index 82c2cca8b7..dba8f4d45b 100644 --- a/Lib/fontTools/voltLib/ast.py +++ b/Lib/fontTools/voltLib/ast.py @@ -317,6 +317,10 @@ class SubstitutionLigatureDefinition(SubstitutionDefinition): pass +class SubstitutionAlternateDefinition(SubstitutionDefinition): + pass + + class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): pass diff --git a/Lib/fontTools/voltLib/parser.py b/Lib/fontTools/voltLib/parser.py index 1fa6b11d02..31a76e69bf 100644 --- a/Lib/fontTools/voltLib/parser.py +++ b/Lib/fontTools/voltLib/parser.py @@ -313,19 +313,27 @@ def parse_substitution_(self, reversal): self.expect_keyword_("END_SUBSTITUTION") max_src = max([len(cov) for cov in src]) max_dest = max([len(cov) for cov in dest]) + # many to many or mixed is invalid - if (max_src > 1 and max_dest > 1) or ( - reversal and (max_src > 1 or max_dest > 1) - ): + if max_src > 1 and max_dest > 1: raise VoltLibError("Invalid substitution type", location) + mapping = dict(zip(tuple(src), tuple(dest))) if max_src == 1 and max_dest == 1: - if reversal: - sub = ast.SubstitutionReverseChainingSingleDefinition( - mapping, location=location - ) + # Alternate substitutions are represented by adding multiple + # substitutions for the same glyph, so we detect that here + glyphs = [x.glyphSet() for cov in src for x in cov] # flatten src + if len(set(glyphs)) != len(glyphs): # src has duplicates + sub = ast.SubstitutionAlternateDefinition(mapping, location=location) else: - sub = ast.SubstitutionSingleDefinition(mapping, location=location) + if reversal: + # Reversal is valid only for single glyph substitutions + # and VOLT ignores it otherwise. + sub = ast.SubstitutionReverseChainingSingleDefinition( + mapping, location=location + ) + else: + sub = ast.SubstitutionSingleDefinition(mapping, location=location) elif max_src == 1 and max_dest > 1: sub = ast.SubstitutionMultipleDefinition(mapping, location=location) elif max_src > 1 and max_dest == 1: diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index c77d5ad111..d552f4b52a 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -46,6 +46,7 @@ import logging import re from io import StringIO +from graphlib import TopologicalSorter from fontTools.feaLib import ast from fontTools.ttLib import TTFont, TTLibError @@ -57,32 +58,39 @@ TABLES = ["GDEF", "GSUB", "GPOS"] -class MarkClassDefinition(ast.MarkClassDefinition): - def asFea(self, indent=""): - res = "" - if not getattr(self, "used", False): - res += "#" - res += ast.MarkClassDefinition.asFea(self, indent) - return res - - -# For sorting voltLib.ast.GlyphDefinition, see its use below. -class Group: - def __init__(self, group): - self.name = group.name.lower() - self.groups = [ - x.group.lower() for x in group.enum.enum if isinstance(x, VAst.GroupName) +def _flatten_group(group): + ret = [] + if isinstance(group, (tuple, list)): + for item in group: + ret.extend(_flatten_group(item)) + elif hasattr(group, "enum"): + ret.extend(_flatten_group(group.enum)) + else: + ret.append(group) + return ret + + +# Topologically sort of group definitions to ensure that all groups are defined +# before they are referenced. This is necessary because FEA requires it but +# VOLT does not, see below. +def sort_groups(groups): + group_map = {group.name.lower(): group for group in groups} + graph = { + group.name.lower(): [ + x.group.lower() + for x in _flatten_group(group) + if isinstance(x, VAst.GroupName) ] + for group in groups + } + sorter = TopologicalSorter(graph) + return [group_map[name] for name in sorter.static_order()] + - def __lt__(self, other): - if self.name in other.groups: - return True - if other.name in self.groups: - return False - if self.groups and not other.groups: - return False - if not self.groups and other.groups: - return True +class Lookup(ast.LookupBlock): + def __init__(self, name, use_extension=False, location=None): + super().__init__(name, use_extension, location) + self.chained = [] class VoltToFea: @@ -90,7 +98,10 @@ class VoltToFea: _NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]") def __init__(self, file_or_path, font=None): - self._file_or_path = file_or_path + if isinstance(file_or_path, VAst.VoltFile): + self._doc, self._file_or_path = file_or_path, None + else: + self._doc, self._file_or_path = None, file_or_path self._font = font self._glyph_map = {} @@ -128,23 +139,26 @@ def _className(self, name): self._class_names[name] = res return self._class_names[name] - def _collectStatements(self, doc, tables): + def _collectStatements(self, doc, tables, ignore_unsupported_settings=False): + # Collect glyph difinitions first, as we need them to map VOLT glyph names to font glyph name. + for statement in doc.statements: + if isinstance(statement, VAst.GlyphDefinition): + self._glyphDefinition(statement) + # Collect and sort group definitions first, to make sure a group # definition that references other groups comes after them since VOLT # does not enforce such ordering, and feature file require it. groups = [s for s in doc.statements if isinstance(s, VAst.GroupDefinition)] - for statement in sorted(groups, key=lambda x: Group(x)): - self._groupDefinition(statement) + for group in sort_groups(groups): + self._groupDefinition(group) for statement in doc.statements: - if isinstance(statement, VAst.GlyphDefinition): - self._glyphDefinition(statement) - elif isinstance(statement, VAst.AnchorDefinition): + if isinstance(statement, VAst.AnchorDefinition): if "GPOS" in tables: self._anchorDefinition(statement) elif isinstance(statement, VAst.SettingDefinition): - self._settingDefinition(statement) - elif isinstance(statement, VAst.GroupDefinition): + self._settingDefinition(statement, ignore_unsupported_settings) + elif isinstance(statement, (VAst.GlyphDefinition, VAst.GroupDefinition)): pass # Handled above elif isinstance(statement, VAst.ScriptDefinition): self._scriptDefinition(statement) @@ -176,35 +190,57 @@ def _buildFeatureFile(self, tables): if self._lookups: statements.append(ast.Comment("\n# Lookups")) for lookup in self._lookups.values(): - statements.extend(getattr(lookup, "targets", [])) + statements.extend(lookup.chained) statements.append(lookup) # Prune features features = self._features.copy() - for ftag in features: - scripts = features[ftag] - for stag in scripts: - langs = scripts[stag] - for ltag in langs: - langs[ltag] = [l for l in langs[ltag] if l.lower() in self._lookups] - scripts[stag] = {t: l for t, l in langs.items() if l} - features[ftag] = {t: s for t, s in scripts.items() if s} + for feature_tag in features: + scripts = features[feature_tag] + for script_tag in scripts: + langs = scripts[script_tag] + for language_tag in langs: + langs[language_tag] = [ + l for l in langs[language_tag] if l.lower() in self._lookups + ] + scripts[script_tag] = {t: l for t, l in langs.items() if l} + features[feature_tag] = {t: s for t, s in scripts.items() if s} features = {t: f for t, f in features.items() if f} if features: statements.append(ast.Comment("# Features")) - for ftag, scripts in features.items(): - feature = ast.FeatureBlock(ftag) - stags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1) - for stag in stags: - feature.statements.append(ast.ScriptStatement(stag)) - ltags = sorted(scripts[stag], key=lambda k: 0 if k == "dflt" else 1) - for ltag in ltags: - include_default = True if ltag == "dflt" else False - feature.statements.append( - ast.LanguageStatement(ltag, include_default=include_default) + for feature_tag, scripts in features.items(): + feature = ast.FeatureBlock(feature_tag) + script_tags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1) + if feature_tag == "aalt" and len(script_tags) > 1: + log.warning( + "FEA syntax does not allow script statements in 'aalt' feature, " + "so only lookups from the first script will be included." + ) + script_tags = script_tags[:1] + for script_tag in script_tags: + if feature_tag != "aalt": + feature.statements.append(ast.ScriptStatement(script_tag)) + language_tags = sorted( + scripts[script_tag], + key=lambda k: 0 if k == "dflt" else 1, + ) + if feature_tag == "aalt" and len(language_tags) > 1: + log.warning( + "FEA syntax does not allow language statements in 'aalt' feature, " + "so only lookups from the first language will be included." ) - for name in scripts[stag][ltag]: + language_tags = language_tags[:1] + for language_tag in language_tags: + if feature_tag != "aalt": + include_default = True if language_tag == "dflt" else False + feature.statements.append( + ast.LanguageStatement( + language_tag.ljust(4), + include_default=include_default, + ) + ) + for name in scripts[script_tag][language_tag]: lookup = self._lookups[name.lower()] lookupref = ast.LookupReferenceStatement(lookup) feature.statements.append(lookupref) @@ -227,15 +263,17 @@ def _buildFeatureFile(self, tables): return doc - def convert(self, tables=None): - doc = VoltParser(self._file_or_path).parse() + def convert(self, tables=None, ignore_unsupported_settings=False): + if self._doc is None: + self._doc = VoltParser(self._file_or_path).parse() + doc = self._doc if tables is None: tables = TABLES if self._font is not None: self._glyph_order = self._font.getGlyphOrder() - self._collectStatements(doc, tables) + self._collectStatements(doc, tables, ignore_unsupported_settings) fea = self._buildFeatureFile(tables) return fea.asFea() @@ -253,7 +291,13 @@ def _groupName(self, group): name = group return ast.GlyphClassName(self._glyphclasses[name.lower()]) - def _coverage(self, coverage): + def _glyphSet(self, item): + return [ + (self._glyphName(x) if isinstance(x, (str, VAst.GlyphName)) else x) + for x in item.glyphSet() + ] + + def _coverage(self, coverage, flatten=False): items = [] for item in coverage: if isinstance(item, VAst.GlyphName): @@ -261,31 +305,38 @@ def _coverage(self, coverage): elif isinstance(item, VAst.GroupName): items.append(self._groupName(item)) elif isinstance(item, VAst.Enum): - items.append(self._enum(item)) + item = self._coverage(item.enum, flatten=True) + if flatten: + items.extend(item) + else: + items.append(ast.GlyphClass(item)) elif isinstance(item, VAst.Range): - items.append((item.start, item.end)) + item = self._glyphSet(item) + if flatten: + items.extend(item) + else: + items.append(ast.GlyphClass(item)) else: raise NotImplementedError(item) return items - def _enum(self, enum): - return ast.GlyphClass(self._coverage(enum.enum)) - def _context(self, context): out = [] for item in context: - coverage = self._coverage(item) - if not isinstance(coverage, (tuple, list)): - coverage = [coverage] - out.extend(coverage) + coverage = self._coverage(item, flatten=True) + if len(coverage) > 1: + coverage = ast.GlyphClass(coverage) + else: + coverage = coverage[0] + out.append(coverage) return out def _groupDefinition(self, group): name = self._className(group.name) - glyphs = self._enum(group.enum) - glyphclass = ast.GlyphClassDefinition(name, glyphs) - - self._glyphclasses[group.name.lower()] = glyphclass + glyphs = self._coverage(group.enum.enum, flatten=True) + glyphclass = ast.GlyphClass(glyphs) + classdef = ast.GlyphClassDefinition(name, glyphclass) + self._glyphclasses[group.name.lower()] = classdef def _glyphDefinition(self, glyph): try: @@ -317,10 +368,10 @@ def _scriptDefinition(self, script): assert ltag not in self._features[ftag][stag] self._features[ftag][stag][ltag] = lookups.keys() - def _settingDefinition(self, setting): + def _settingDefinition(self, setting, ignore_unsupported=False): if setting.name.startswith("COMPILER_"): self._settings[setting.name] = setting.value - else: + elif not ignore_unsupported: log.warning(f"Unsupported setting ignored: {setting.name}") def _adjustment(self, adjustment): @@ -358,18 +409,15 @@ def _anchorDefinition(self, anchordef): glyphname = anchordef.glyph_name anchor = self._anchor(anchordef.pos) + if glyphname not in self._anchors: + self._anchors[glyphname] = {} if anchorname.startswith("MARK_"): - name = "_".join(anchorname.split("_")[1:]) - markclass = ast.MarkClass(self._className(name)) - glyph = self._glyphName(glyphname) - markdef = MarkClassDefinition(markclass, anchor, glyph) - self._markclasses[(glyphname, anchorname)] = markdef + anchorname = anchorname[:5] + anchorname[5:].lower() else: - if glyphname not in self._anchors: - self._anchors[glyphname] = {} - if anchorname not in self._anchors[glyphname]: - self._anchors[glyphname][anchorname] = {} - self._anchors[glyphname][anchorname][anchordef.component] = anchor + anchorname = anchorname.lower() + if anchorname not in self._anchors[glyphname]: + self._anchors[glyphname][anchorname] = {} + self._anchors[glyphname][anchorname][anchordef.component] = anchor def _gposLookup(self, lookup, fealookup): statements = fealookup.statements @@ -408,43 +456,66 @@ def _gposLookup(self, lookup, fealookup): ) elif isinstance(pos, VAst.PositionAttachDefinition): anchors = {} - for marks, classname in pos.coverage_to: - for mark in marks: - # Set actually used mark classes. Basically a hack to get - # around the feature file syntax limitation of making mark - # classes global and not allowing mark positioning to - # specify mark coverage. - for name in mark.glyphSet(): - key = (name, "MARK_" + classname) - self._markclasses[key].used = True - markclass = ast.MarkClass(self._className(classname)) + allmarks = set() + for coverage, anchorname in pos.coverage_to: + # In feature files mark classes are global, but in VOLT they + # are defined per-lookup. If we output mark class definitions + # for all marks that use a given anchor, we might end up with a + # mark used in two different classes in the same lookup, which + # is causes feature file compilation error. + # At the expense of uglier feature code, we make the mark class + # name by appending the current lookup name not the anchor + # name, and output mark class definitions only for marks used + # in this lookup. + classname = self._className(f"{anchorname}.{lookup.name}") + markclass = ast.MarkClass(classname) + + # Anchor names are case-insensitive in VOLT + anchorname = anchorname.lower() + + # We might still end in marks used in two different anchor + # classes, so we filter out already used marks. + marks = set() + for mark in coverage: + marks.update(mark.glyphSet()) + if not marks.isdisjoint(allmarks): + marks.difference_update(allmarks) + if not marks: + continue + allmarks.update(marks) + + for glyphname in marks: + glyph = self._glyphName(glyphname) + anchor = self._anchors[glyphname][f"MARK_{anchorname}"][1] + markdef = ast.MarkClassDefinition(markclass, anchor, glyph) + self._markclasses[(glyphname, classname)] = markdef + for base in pos.coverage: for name in base.glyphSet(): if name not in anchors: anchors[name] = [] - if classname not in anchors[name]: - anchors[name].append(classname) + if (anchorname, classname) not in anchors[name]: + anchors[name].append((anchorname, classname)) + is_ligature = all(n in self._ligatures for n in anchors) + is_mark = all(n in self._marks for n in anchors) for name in anchors: components = 1 - if name in self._ligatures: + if is_ligature: components = self._ligatures[name] - marks = [] - for mark in anchors[name]: - markclass = ast.MarkClass(self._className(mark)) + marks = [[] for _ in range(components)] + for mark, classname in anchors[name]: + markclass = ast.MarkClass(classname) for component in range(1, components + 1): - if len(marks) < component: - marks.append([]) - anchor = None if component in self._anchors[name][mark]: anchor = self._anchors[name][mark][component] - marks[component - 1].append((anchor, markclass)) + marks[component - 1].append((anchor, markclass)) base = self._glyphName(name) - if name in self._marks: + if is_mark: mark = ast.MarkMarkPosStatement(base, marks[0]) - elif name in self._ligatures: + elif is_ligature: mark = ast.MarkLigPosStatement(base, marks) else: mark = ast.MarkBasePosStatement(base, marks[0]) @@ -481,13 +552,9 @@ def _gposLookup(self, lookup, fealookup): else: raise NotImplementedError(pos) - def _gposContextLookup( - self, lookup, prefix, suffix, ignore, fealookup, targetlookup - ): + def _gposContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained): statements = fealookup.statements - assert not lookup.reversal - pos = lookup.pos if isinstance(pos, VAst.PositionAdjustPairDefinition): for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items(): @@ -500,79 +567,181 @@ def _gposContextLookup( if ignore: statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) else: - lookups = (targetlookup, targetlookup) statement = ast.ChainContextPosStatement( - prefix, glyphs, suffix, lookups + prefix, glyphs, suffix, [chained, chained] ) statements.append(statement) elif isinstance(pos, VAst.PositionAdjustSingleDefinition): glyphs = [ast.GlyphClass()] - for a, b in pos.adjust_single: - glyph = self._coverage(a) - glyphs[0].extend(glyph) + for a, _ in pos.adjust_single: + glyphs[0].extend(self._coverage(a, flatten=True)) if ignore: statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) else: statement = ast.ChainContextPosStatement( - prefix, glyphs, suffix, [targetlookup] + prefix, glyphs, suffix, [chained] ) statements.append(statement) elif isinstance(pos, VAst.PositionAttachDefinition): glyphs = [ast.GlyphClass()] for coverage, _ in pos.coverage_to: - glyphs[0].extend(self._coverage(coverage)) + glyphs[0].extend(self._coverage(coverage, flatten=True)) if ignore: statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) else: statement = ast.ChainContextPosStatement( - prefix, glyphs, suffix, [targetlookup] + prefix, glyphs, suffix, [chained] ) statements.append(statement) else: raise NotImplementedError(pos) - def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): + def _gsubLookup(self, lookup, fealookup): statements = fealookup.statements sub = lookup.sub + + # Alternate substitutions are represented by adding multiple + # substitutions for the same glyph, so we need to collect them into one + # to many mapping. + if isinstance(sub, VAst.SubstitutionAlternateDefinition): + alternates = {} + for key, val in sub.mapping.items(): + if not key or not val: + path, line, column = sub.location + log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") + continue + glyphs = self._coverage(key) + replacements = self._coverage(val) + assert len(glyphs) == 1 + for src_glyph, repl_glyph in zip( + glyphs[0].glyphSet(), replacements[0].glyphSet() + ): + alternates.setdefault(str(self._glyphName(src_glyph)), []).append( + str(self._glyphName(repl_glyph)) + ) + + for glyph, replacements in alternates.items(): + statement = ast.AlternateSubstStatement( + [], glyph, [], ast.GlyphClass(replacements) + ) + statements.append(statement) + return + for key, val in sub.mapping.items(): if not key or not val: path, line, column = sub.location log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") continue - statement = None glyphs = self._coverage(key) replacements = self._coverage(val) - if ignore: - chain_context = (prefix, glyphs, suffix) - statement = ast.IgnoreSubstStatement([chain_context]) - elif isinstance(sub, VAst.SubstitutionSingleDefinition): + if isinstance(sub, VAst.SubstitutionSingleDefinition): assert len(glyphs) == 1 assert len(replacements) == 1 - statement = ast.SingleSubstStatement( - glyphs, replacements, prefix, suffix, chain + statements.append( + ast.SingleSubstStatement(glyphs, replacements, [], [], False) ) elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition): - assert len(glyphs) == 1 - assert len(replacements) == 1 - statement = ast.ReverseChainSingleSubstStatement( - prefix, suffix, glyphs, replacements - ) + # This is handled in gsubContextLookup() + pass elif isinstance(sub, VAst.SubstitutionMultipleDefinition): assert len(glyphs) == 1 - statement = ast.MultipleSubstStatement( - prefix, glyphs[0], suffix, replacements, chain + statements.append( + ast.MultipleSubstStatement([], glyphs[0], [], replacements) ) elif isinstance(sub, VAst.SubstitutionLigatureDefinition): assert len(replacements) == 1 statement = ast.LigatureSubstStatement( - prefix, glyphs, suffix, replacements[0], chain + [], glyphs, [], replacements[0], False ) + + # If any of the input glyphs is a group, we need to + # explode the substitution into multiple ligature substitutions + # since feature file syntax does not support classes in + # ligature substitutions. + n = max(len(x.glyphSet()) for x in glyphs) + if n > 1: + # All input should either be groups of the same length or single glyphs + assert all(len(x.glyphSet()) in (n, 1) for x in glyphs) + glyphs = [x.glyphSet() for x in glyphs] + glyphs = [([x[0]] * n if len(x) == 1 else x) for x in glyphs] + + # In this case ligature replacements must be a group of the same length + # as the input groups, or a single glyph. VOLT + # allows the replacement glyphs to be longer and truncates them. + # So well allow that and zip() below will do the truncation + # for us. + replacement = replacements[0].glyphSet() + if len(replacement) == 1: + replacement = [replacement[0]] * n + assert len(replacement) >= n + + # Add the unexploded statement commented out for reference. + statements.append(ast.Comment(f"# {statement}")) + + for zipped in zip(*glyphs, replacement): + zipped = [self._glyphName(x) for x in zipped] + statements.append( + ast.LigatureSubstStatement( + [], zipped[:-1], [], zipped[-1], False + ) + ) + else: + statements.append(statement) else: raise NotImplementedError(sub) - statements.append(statement) + + def _gsubContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained): + statements = fealookup.statements + + sub = lookup.sub + + if isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition): + # Reverse substitutions is a special case, it can’t use chained lookups. + for key, val in sub.mapping.items(): + if not key or not val: + path, line, column = sub.location + log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") + continue + glyphs = self._coverage(key) + replacements = self._coverage(val) + statements.append( + ast.ReverseChainSingleSubstStatement( + prefix, suffix, glyphs, replacements + ) + ) + fealookup.chained = [] + return + + if not isinstance( + sub, + ( + VAst.SubstitutionSingleDefinition, + VAst.SubstitutionMultipleDefinition, + VAst.SubstitutionLigatureDefinition, + VAst.SubstitutionAlternateDefinition, + ), + ): + raise NotImplementedError(type(sub)) + + glyphs = [] + for key, val in sub.mapping.items(): + if not key or not val: + path, line, column = sub.location + log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") + continue + glyphs.extend(self._coverage(key, flatten=True)) + + if len(glyphs) > 1: + glyphs = [ast.GlyphClass(glyphs)] + if ignore: + statements.append(ast.IgnoreSubstStatement([(prefix, glyphs, suffix)])) + else: + statements.append( + ast.ChainContextSubstStatement(prefix, glyphs, suffix, [chained]) + ) def _lookupDefinition(self, lookup): mark_attachement = None @@ -598,13 +767,21 @@ def _lookupDefinition(self, lookup): lookupflags = ast.LookupFlagStatement( flags, mark_attachement, mark_filtering ) + + use_extension = False + if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"): + use_extension = True + if "\\" in lookup.name: # Merge sub lookups as subtables (lookups named “base\sub”), # makeotf/feaLib will issue a warning and ignore the subtable # statement if it is not a pairpos lookup, though. name = lookup.name.split("\\")[0] if name.lower() not in self._lookups: - fealookup = ast.LookupBlock(self._lookupName(name)) + fealookup = Lookup( + self._lookupName(name), + use_extension=use_extension, + ) if lookupflags is not None: fealookup.statements.append(lookupflags) fealookup.statements.append(ast.Comment("# " + lookup.name)) @@ -614,7 +791,10 @@ def _lookupDefinition(self, lookup): fealookup.statements.append(ast.Comment("# " + lookup.name)) self._lookups[name.lower()] = fealookup else: - fealookup = ast.LookupBlock(self._lookupName(lookup.name)) + fealookup = Lookup( + self._lookupName(lookup.name), + use_extension=use_extension, + ) if lookupflags is not None: fealookup.statements.append(lookupflags) self._lookups[lookup.name.lower()] = fealookup @@ -623,39 +803,40 @@ def _lookupDefinition(self, lookup): fealookup.statements.append(ast.Comment("# " + lookup.comments)) contexts = [] - if lookup.context: - for context in lookup.context: - prefix = self._context(context.left) - suffix = self._context(context.right) - ignore = context.ex_or_in == "EXCEPT_CONTEXT" - contexts.append([prefix, suffix, ignore, False]) - # It seems that VOLT will create contextual substitution using - # only the input if there is no other contexts in this lookup. - if ignore and len(lookup.context) == 1: - contexts.append([[], [], False, True]) - else: - contexts.append([[], [], False, False]) - - targetlookup = None - for prefix, suffix, ignore, chain in contexts: + for context in lookup.context: + prefix = self._context(context.left) + suffix = self._context(context.right) + ignore = context.ex_or_in == "EXCEPT_CONTEXT" + contexts.append([prefix, suffix, ignore]) + # It seems that VOLT will create contextual substitution using + # only the input if there is no other contexts in this lookup. + if ignore and len(lookup.context) == 1: + contexts.append([[], [], False]) + + if contexts: + chained = ast.LookupBlock( + self._lookupName(lookup.name + " chained"), + use_extension=use_extension, + ) + fealookup.chained.append(chained) if lookup.sub is not None: - self._gsubLookup(lookup, prefix, suffix, ignore, chain, fealookup) - - if lookup.pos is not None: - if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"): - fealookup.use_extension = True - if prefix or suffix or chain or ignore: - if not ignore and targetlookup is None: - targetname = self._lookupName(lookup.name + " target") - targetlookup = ast.LookupBlock(targetname) - fealookup.targets = getattr(fealookup, "targets", []) - fealookup.targets.append(targetlookup) - self._gposLookup(lookup, targetlookup) + self._gsubLookup(lookup, chained) + elif lookup.pos is not None: + self._gposLookup(lookup, chained) + for prefix, suffix, ignore in contexts: + if lookup.sub is not None: + self._gsubContextLookup( + lookup, prefix, suffix, ignore, fealookup, chained + ) + elif lookup.pos is not None: self._gposContextLookup( - lookup, prefix, suffix, ignore, fealookup, targetlookup + lookup, prefix, suffix, ignore, fealookup, chained ) - else: - self._gposLookup(lookup, fealookup) + else: + if lookup.sub is not None: + self._gsubLookup(lookup, fealookup) + elif lookup.pos is not None: + self._gposLookup(lookup, fealookup) def main(args=None): diff --git a/Makefile b/Makefile deleted file mode 100644 index 21cad6cd20..0000000000 --- a/Makefile +++ /dev/null @@ -1,25 +0,0 @@ -all: - ./setup.py build - -dist: - ./setup.py sdist bdist_wheel - -install: - pip install --ignore-installed . - -install-user: - pip install --ignore-installed --user . - -uninstall: - pip uninstall --yes fonttools - -check: all - pytest - -clean: - ./setup.py clean --all - -docs: - cd Doc && $(MAKE) html - -.PHONY: all dist install install-user uninstall check clean docs diff --git a/NEWS.rst b/NEWS.rst index c16e31e0ae..a6af57ee70 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,41 @@ +4.58.0 (released 2025-05-10) +---------------------------- + +- Drop Python 3.8, require 3.9+ (#3819) +- [HVAR, VVAR] Prune unused regions when using a direct mapping (#3797) +- [Docs] Improvements to ufoLib documentation (#3721) +- [Docs] Improvements to varLib documentation (#3727) +- [Docs] Improvements to Pens and pen-module documentation (#3724) +- [Docs] Miscellany updates to docs (misc modules and smaller modules) (#3730) +- [subset] Close codepoints over BiDi mirror variants. (#3801) +- [feaLib] Fix serializing ChainContextPosStatement and + ChainContextSubstStatement in some rare cases (#3788) +- [designspaceLib] Clarify user expectations for getStatNames (#2892) +- [GVAR] Add support for new `GVAR` table (#3728) +- [TSI0, TSI5] Derive number of entries to decompile from data length (#2477) +- [ttLib] Fix `AttributeError` when reporting table overflow (#3808) +- [ttLib] Apply rounding more often in getCoordinates (#3798) +- [ttLib] Ignore component bounds if empty (#3799) +- [ttLib] Change the separator for duplicate glyph names from "#" to "." (#3809) +- [feaLib] Support subtable breaks in CursivePos, MarkBasePos, MarkToLigPos and + MarkToMarkPos lookups (#3800, #3807) +- [feaLib] If the same lookup has single substitutions and ligature + substitutions, upgrade single substitutions to ligature substitutions with + one input glyph (#3805) +- [feaLib] Correctly handle in single pos lookups (#3803) +- [feaLib] Remove duplicates from class pair pos classes instead of raising an + error (#3804) +- [feaLib] Support creating extension lookups using useExtenion lookup flag + instead of silently ignoring it (#3811) +- [STAT] Add typing for the simpler STAT arguments (#3812) +- [otlLib.builder] Add future import for annotations (#3814) +- [cffLib] Fix reading supplement encoding (#3813) +- [voltLib] Add some missing functionality and fixes to voltLib and VoltToFea, + making the conversion to feature files more robust. Add also `fonttools + voltLib` command line tool to compile VOLT sources directly (doing an + intermediate fea conversion internally) (#3818) +- [pens] Add some PointPen annotations (#3820) + 4.57.0 (released 2025-04-03) ---------------------------- diff --git a/README.rst b/README.rst index b604ea7ca5..e40554dae8 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ What is this? 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 - licence `__. + license `__. | Among other things this means you can use it free of charge. `User documentation `_ and @@ -18,7 +18,7 @@ are available at `Read the Docs `_. Installation ~~~~~~~~~~~~ -FontTools requires `Python `__ 3.8 +FontTools requires `Python `__ 3.9 or later. We try to follow the same schedule of minimum Python version support as NumPy (see `NEP 29 `__). @@ -88,9 +88,6 @@ are required to unlock the extra features named "ufo", etc. * `fs `__: (aka ``pyfilesystem2``) filesystem abstraction layer. - * `enum34 `__: backport for the built-in ``enum`` - module (only required on Python < 3.4). - *Extra:* ``ufo`` - ``Lib/fontTools/ttLib/woff2.py`` @@ -255,7 +252,7 @@ How to make a new release automate that too. -Acknowledgements +Acknowledgments ~~~~~~~~~~~~~~~~ In alphabetical order: @@ -266,7 +263,7 @@ Vincent Connare, David Corbett, Simon Cozens, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod, Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Takaaki Fuji, Rob Hagemans, 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 +Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, Liang Hai, Peter Lofting, Cosimo Lupo, Olli Meier, Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret Rieger, Read Roberts, Colin Rofls, Guido van Rossum, Just van Rossum, Andreas Seidel, Georg Seifert, Chris Simpkins, Miguel Sousa, diff --git a/Tests/cffLib/cffLib_test.py b/Tests/cffLib/cffLib_test.py index 7146e5d660..8f30fbf26d 100644 --- a/Tests/cffLib/cffLib_test.py +++ b/Tests/cffLib/cffLib_test.py @@ -1,9 +1,15 @@ -from fontTools.cffLib import TopDict, PrivateDict, CharStrings +import os +import sys + +libdir = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), "Lib" +) +sys.path.insert(0, libdir) + +from fontTools.cffLib import TopDict, PrivateDict, CharStrings, CFFFontSet from fontTools.misc.testTools import parseXML, DataFilesHandler from fontTools.ttLib import TTFont import copy -import os -import sys import unittest from io import BytesIO @@ -119,9 +125,18 @@ def test_unique_glyph_names(self): glyphOrder = font2.getGlyphOrder() self.assertEqual(len(glyphOrder), len(set(glyphOrder))) + def test_reading_supplement_encoding(self): + cff_path = self.getpath("TestSupplementEncoding.cff") + topDict = None + with open(cff_path, "rb") as fontfile: + cff = CFFFontSet() + cff.decompile(fontfile, None) + topDict = cff[0] + self.assertEqual(topDict.Encoding[9], "space") + self.assertEqual(topDict.Encoding[32], "space") -class CFFToCFF2Test(DataFilesHandler): +class CFFToCFF2Test(DataFilesHandler): def test_conversion(self): font_path = self.getpath("CFFToCFF2-1.otf") font = TTFont(font_path) diff --git a/Tests/cffLib/data/TestSupplementEncoding.cff b/Tests/cffLib/data/TestSupplementEncoding.cff new file mode 100644 index 0000000000..0e43582f00 Binary files /dev/null and b/Tests/cffLib/data/TestSupplementEncoding.cff differ diff --git a/Tests/feaLib/ast_test.py b/Tests/feaLib/ast_test.py index ebae3dab27..4701e01011 100644 --- a/Tests/feaLib/ast_test.py +++ b/Tests/feaLib/ast_test.py @@ -13,12 +13,34 @@ def test_valuerecord_none(self): statement = ast.ValueRecord(xPlacement=10, xAdvance=20) self.assertEqual(statement.asFea(), "<10 0 20 0>") + def test_valuerecord_empty(self): + statement = ast.ValueRecord() + self.assertEqual(statement.asFea(), "") + def test_non_object_location(self): el = ast.Element(location=("file.fea", 1, 2)) self.assertEqual(el.location.file, "file.fea") self.assertEqual(el.location.line, 1) self.assertEqual(el.location.column, 2) + def test_single_pos_statement_empty_valuerecord(self): + statement = ast.SinglePosStatement( + pos=[(ast.GlyphName("a"), ast.ValueRecord())], + prefix=[], + suffix=[], + forceChain=False, + ) + self.assertEqual(statement.asFea(), "pos a ;") + + def test_single_pos_statement_empty_valuerecord_chain(self): + statement = ast.SinglePosStatement( + pos=[(ast.GlyphName("a"), ast.ValueRecord())], + prefix=[], + suffix=[], + forceChain=True, + ) + self.assertEqual(statement.asFea(), "pos a' ;") + if __name__ == "__main__": import sys diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 402b5c41c0..aa869806fd 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -68,7 +68,7 @@ class BuilderTest(unittest.TestCase): spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 spec5h1 spec6b_ii spec6d2 spec6e spec6f - spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d + spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8a_2 spec8b spec8c spec8d spec9a spec9a2 spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g spec10 bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509 @@ -87,6 +87,13 @@ class BuilderTest(unittest.TestCase): contextual_inline_multi_sub_format_2 contextual_inline_format_4 duplicate_language_stmt + CursivePosSubtable + MarkBasePosSubtable + MarkLigPosSubtable + MarkMarkPosSubtable + single_pos_NULL + class_pair_pos_duplicates + useExtension """.split() VARFONT_AXES = [ @@ -299,6 +306,71 @@ def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)" ) + def test_mixed_singleSubst_multipleSubst(self): + font = self.build( + "lookup test {" + " sub f_f by f f;" + " sub f by f;" + " sub f_f_i by f f i;" + " sub [A A.sc] by A;" + " sub [B B.sc] by [B B.sc];" + "} test;" + ) + + assert "GSUB" in font + st = font["GSUB"].table.LookupList.Lookup[0].SubTable[0] + self.assertEqual(st.LookupType, 2) + self.assertEqual( + st.mapping, + { + "f_f": ("f", "f"), + "f": ("f",), + "f_f_i": ("f", "f", "i"), + "A": ("A",), + "A.sc": ("A",), + "B": ("B",), + "B.sc": ("B.sc",), + }, + ) + + def test_mixed_singleSubst_ligatureSubst(self): + font = self.build( + "lookup test {" + " sub f f by f_f;" + " sub f f i by f_f_i;" + " sub A by A.sc;" + "} test;" + ) + + assert "GSUB" in font + st = font["GSUB"].table.LookupList.Lookup[0].SubTable[0] + self.assertEqual(st.LookupType, 4) + self.assertEqual(len(st.ligatures), 2) + self.assertEqual(len(st.ligatures["f"]), 2) + self.assertEqual(st.ligatures["f"][0].LigGlyph, "f_f_i") + self.assertEqual(len(st.ligatures["f"][0].Component), 2) + self.assertEqual(st.ligatures["f"][0].Component[0], "f") + self.assertEqual(st.ligatures["f"][0].Component[1], "i") + self.assertEqual(st.ligatures["f"][1].LigGlyph, "f_f") + self.assertEqual(len(st.ligatures["f"][1].Component), 1) + self.assertEqual(st.ligatures["f"][1].Component[0], "f") + self.assertEqual(len(st.ligatures["A"]), 1) + self.assertEqual(st.ligatures["A"][0].LigGlyph, "A.sc") + self.assertEqual(len(st.ligatures["A"][0].Component), 0) + + def test_mixed_singleSubst_multipleSubst_ligatureSubst(self): + self.assertRaisesRegex( + FeatureLibError, + "Within a named lookup block, all rules must be of the " + "same lookup type and flag", + self.build, + "lookup test {" + " sub A by A.sc;" + " sub f_f by f f;" + " sub f f i by f_f_i;" + "} test;", + ) + def test_pairPos_redefinition_warning(self): # https://github.com/fonttools/fonttools/issues/1147 logger = logging.getLogger("fontTools.otlLib.builder") @@ -1179,6 +1251,14 @@ def test_variable_anchors_round_trip(self): addOpenTypeFeatures(font, feafile) assert dedent(str(feafile)) == dedent(features) + def test_feature_useExtension(self): + self.assertRaisesRegex( + FeatureLibError, + "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature", + self.build, + "feature liga useExtension { sub f f by f_f; } liga;", + ) + def generate_feature_file_test(name): return lambda self: self.check_feature_file(name) diff --git a/Tests/feaLib/data/CursivePosSubtable.fea b/Tests/feaLib/data/CursivePosSubtable.fea new file mode 100644 index 0000000000..89fb09c79d --- /dev/null +++ b/Tests/feaLib/data/CursivePosSubtable.fea @@ -0,0 +1,9 @@ +feature test { + pos cursive A ; + subtable; + pos cursive a ; + subtable; + pos cursive B ; + subtable; + pos cursive b ; +} test; diff --git a/Tests/feaLib/data/CursivePosSubtable.ttx b/Tests/feaLib/data/CursivePosSubtable.ttx new file mode 100644 index 0000000000..3828d21bc1 --- /dev/null +++ b/Tests/feaLib/data/CursivePosSubtable.ttx @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/MarkBasePosSubtable.fea b/Tests/feaLib/data/MarkBasePosSubtable.fea new file mode 100644 index 0000000000..ca338741dc --- /dev/null +++ b/Tests/feaLib/data/MarkBasePosSubtable.fea @@ -0,0 +1,18 @@ +languagesystem DFLT dflt; + +markClass [acute grave] @TOP_MARKS; +markClass macron @TOP_MARKS; +markClass [cedilla] @BOTTOM_MARKS; +markClass [ogonek] @SIDE_MARKS; + +feature test { + pos base a + mark @TOP_MARKS + mark @BOTTOM_MARKS; + subtable; + pos base [b c] + mark @BOTTOM_MARKS; + subtable; + pos base d + mark @SIDE_MARKS; +} test; diff --git a/Tests/feaLib/data/MarkBasePosSubtable.ttx b/Tests/feaLib/data/MarkBasePosSubtable.ttx new file mode 100644 index 0000000000..69e4c1da80 --- /dev/null +++ b/Tests/feaLib/data/MarkBasePosSubtable.ttx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/MarkLigPosSubtable.fea b/Tests/feaLib/data/MarkLigPosSubtable.fea new file mode 100644 index 0000000000..acd6b69b93 --- /dev/null +++ b/Tests/feaLib/data/MarkLigPosSubtable.fea @@ -0,0 +1,25 @@ +languagesystem DFLT dflt; + +markClass [acute grave] @TOP_MARKS; +markClass macron @TOP_MARKS; +markClass [cedilla] @BOTTOM_MARKS; +markClass [ogonek] @SIDE_MARKS; + +feature test { + pos ligature f_f + mark @TOP_MARKS + mark @BOTTOM_MARKS + ligComponent + mark @TOP_MARKS + mark @BOTTOM_MARKS; + subtable; + pos ligature [f_i f_l] + mark @BOTTOM_MARKS + ligComponent + mark @BOTTOM_MARKS; + subtable; + pos ligature T_h + mark @SIDE_MARKS + ligComponent + mark @SIDE_MARKS; +} test; diff --git a/Tests/feaLib/data/MarkLigPosSubtable.ttx b/Tests/feaLib/data/MarkLigPosSubtable.ttx new file mode 100644 index 0000000000..0d03a359cc --- /dev/null +++ b/Tests/feaLib/data/MarkLigPosSubtable.ttx @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/MarkMarkPosSubtable.fea b/Tests/feaLib/data/MarkMarkPosSubtable.fea new file mode 100644 index 0000000000..d8bacdba5a --- /dev/null +++ b/Tests/feaLib/data/MarkMarkPosSubtable.fea @@ -0,0 +1,18 @@ +languagesystem DFLT dflt; + +markClass [acute grave] @TOP_MARKS; +markClass macron @TOP_MARKS; +markClass [cedilla] @BOTTOM_MARKS; +markClass [ogonek] @SIDE_MARKS; + +feature test { + pos mark acute + mark @TOP_MARKS + mark @BOTTOM_MARKS; + subtable; + pos mark [grave cedilla] + mark @BOTTOM_MARKS; + subtable; + pos mark ogonek + mark @SIDE_MARKS; +} test; diff --git a/Tests/feaLib/data/MarkMarkPosSubtable.ttx b/Tests/feaLib/data/MarkMarkPosSubtable.ttx new file mode 100644 index 0000000000..f01e0659eb --- /dev/null +++ b/Tests/feaLib/data/MarkMarkPosSubtable.ttx @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/class_pair_pos_duplicates.fea b/Tests/feaLib/data/class_pair_pos_duplicates.fea new file mode 100644 index 0000000000..b549026c56 --- /dev/null +++ b/Tests/feaLib/data/class_pair_pos_duplicates.fea @@ -0,0 +1,4 @@ +# This should result in a ValueFormat 0 +feature test { + pos [A A B C] [A A B C] -100; +} test; diff --git a/Tests/feaLib/data/class_pair_pos_duplicates.ttx b/Tests/feaLib/data/class_pair_pos_duplicates.ttx new file mode 100644 index 0000000000..14a97e336c --- /dev/null +++ b/Tests/feaLib/data/class_pair_pos_duplicates.ttx @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/single_pos_NULL.fea b/Tests/feaLib/data/single_pos_NULL.fea new file mode 100644 index 0000000000..4e58b7b83f --- /dev/null +++ b/Tests/feaLib/data/single_pos_NULL.fea @@ -0,0 +1,8 @@ +# Both lookups should result in SinglePos lookup with a ValueFormat 0 +lookup test1 { + pos A ; +} test1; + +lookup test2 { + pos B' ; +} test2; diff --git a/Tests/feaLib/data/single_pos_NULL.ttx b/Tests/feaLib/data/single_pos_NULL.ttx new file mode 100644 index 0000000000..2ba2c24439 --- /dev/null +++ b/Tests/feaLib/data/single_pos_NULL.ttx @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/spec8a.ttx b/Tests/feaLib/data/spec8a.ttx index 9c8c758ef7..569219eba6 100644 --- a/Tests/feaLib/data/spec8a.ttx +++ b/Tests/feaLib/data/spec8a.ttx @@ -69,27 +69,28 @@ - - - + + + - + - - + + + @@ -102,15 +103,6 @@ - - - - - - - - - @@ -119,26 +111,25 @@ - - - - - - - - - - + + + + + + + + + - + @@ -150,7 +141,7 @@ - + @@ -172,11 +163,11 @@ - + - + @@ -186,7 +177,7 @@ - + diff --git a/Tests/feaLib/data/spec8a_2.fea b/Tests/feaLib/data/spec8a_2.fea new file mode 100644 index 0000000000..68f558a45c --- /dev/null +++ b/Tests/feaLib/data/spec8a_2.fea @@ -0,0 +1,21 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn TRK; +languagesystem cyrl dflt; + +feature aalt useExtension { + feature salt; + feature smcp; + sub d by d.alt; +} aalt; + +feature smcp { + sub [a - c] by [A.sc - C.sc]; + sub f i by f_i; # not considered for aalt +} smcp; + +feature salt { + sub a from [a.alt1 a.alt2 a.alt3]; + sub e [c d e]' f by [c.mid d.mid e.mid]; + sub b by b.alt; +} salt; diff --git a/Tests/feaLib/data/spec8a_2.ttx b/Tests/feaLib/data/spec8a_2.ttx new file mode 100644 index 0000000000..5cb4a4a410 --- /dev/null +++ b/Tests/feaLib/data/spec8a_2.ttx @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/useExtension.fea b/Tests/feaLib/data/useExtension.fea new file mode 100644 index 0000000000..d2eef02280 --- /dev/null +++ b/Tests/feaLib/data/useExtension.fea @@ -0,0 +1,32 @@ +languagesystem DFLT dflt; + +lookup liga1 useExtension { + sub f i by f_i; +} liga1; + +lookup liga2 { + sub f l by f_l; +} liga2; + +lookup liga3 useExtension { + sub a f' f' i' by f_f_i; +} liga3; + +feature liga { + lookup liga1; + lookup liga2; + lookup liga3; +} liga; + +lookup kern1 useExtension { + pos A V -100; +} kern1; + +lookup kern2 { + pos V A -100; +} kern2; + +feature kern { + lookup kern1; + lookup kern2; +} kern; \ No newline at end of file diff --git a/Tests/feaLib/data/useExtension.ttx b/Tests/feaLib/data/useExtension.ttx new file mode 100644 index 0000000000..2dedede70a --- /dev/null +++ b/Tests/feaLib/data/useExtension.ttx @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py index bee00d9d71..5f89fe500e 100644 --- a/Tests/feaLib/parser_test.py +++ b/Tests/feaLib/parser_test.py @@ -1021,6 +1021,16 @@ def test_gpos_type_1_chained_exception4(self): with self.assertRaisesRegex(FeatureLibError, "Positioning values are allowed"): doc = self.parse("feature kern {" " pos a' b c 123 d;" "} kern;") + def test_gpos_type_1_null(self): + doc = self.parse("feature test {pos a ;} test;") + pos = doc.statements[0].statements[0] + self.assertEqual(pos.asFea(), "pos a ;") + + def test_gpos_type_1_null_chained(self): + doc = self.parse("feature test {pos a' ;} test;") + pos = doc.statements[0].statements[0] + self.assertEqual(pos.asFea(), "pos a' ;") + def test_gpos_type_2_format_a(self): doc = self.parse( "feature kern {" " pos [T V] -60 [a b c] <1 2 3 4>;" "} kern;" @@ -1717,30 +1727,6 @@ def test_split_marked_glyphs_runs(self): "} test;", ) - def test_substitute_mix_single_multiple(self): - doc = self.parse( - "lookup Look {" - " sub f_f by f f;" - " sub f by f;" - " sub f_f_i by f f i;" - " sub [a a.sc] by a;" - " sub [a a.sc] by [b b.sc];" - "} Look;" - ) - statements = doc.statements[0].statements - for sub in statements: - self.assertIsInstance(sub, ast.MultipleSubstStatement) - self.assertEqual(statements[1].glyph, "f") - self.assertEqual(statements[1].replacement, ["f"]) - self.assertEqual(statements[3].glyph, "a") - self.assertEqual(statements[3].replacement, ["a"]) - self.assertEqual(statements[4].glyph, "a.sc") - self.assertEqual(statements[4].replacement, ["a"]) - self.assertEqual(statements[5].glyph, "a") - self.assertEqual(statements[5].replacement, ["b"]) - self.assertEqual(statements[6].glyph, "a.sc") - self.assertEqual(statements[6].replacement, ["b.sc"]) - def test_substitute_from(self): # GSUB LookupType 3 doc = self.parse( "feature test {" " substitute a from [a.1 a.2 a.3];" "} test;" diff --git a/Tests/misc/py23_test.py b/Tests/misc/py23_test.py index 30382455b7..9c551dff21 100644 --- a/Tests/misc/py23_test.py +++ b/Tests/misc/py23_test.py @@ -40,9 +40,10 @@ def diff_piped(self, data, import_statement): script = self.make_temp("\n".join([import_statement, PIPE_SCRIPT])) datafile = self.make_temp(data) try: - with open(datafile, "rb") as infile, tempfile.NamedTemporaryFile( - delete=False - ) as outfile: + with ( + open(datafile, "rb") as infile, + tempfile.NamedTemporaryFile(delete=False) as outfile, + ): env = dict(os.environ) env["PYTHONPATH"] = os.pathsep.join(sys.path) check_call( @@ -60,15 +61,8 @@ def test_binary_pipe_py23_open_wrapper(self): self.fail("Input and output data differ!") def test_binary_pipe_built_in_io_open(self): - if sys.version_info.major < 3 and sys.platform == "win32": - # On Windows Python 2.x, the piped input and output data are - # expected to be different when using io.open, because of issue - # https://bugs.python.org/issue10841. - expected = True - else: - expected = False result = self.diff_piped(TEST_BIN_DATA, "from io import open") - self.assertEqual(result, expected) + self.assertEqual(result, False) class Round2Test(unittest.TestCase): diff --git a/Tests/subset/data/expect_keep_math.ttx b/Tests/subset/data/expect_keep_math.ttx index f2bc41dfba..1b0dd41293 100644 --- a/Tests/subset/data/expect_keep_math.ttx +++ b/Tests/subset/data/expect_keep_math.ttx @@ -1,26 +1,34 @@ - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -174,6 +182,47 @@ -355 -286 -253 -578 0 -673 0 -675 253 -577 355 -286 rrcurveto endchar + + -268 656 20 hstem + 199 86 vstem + 29 660 rmoveto + 145 -114 25 -115 0 -187 0 -194 -28 -94 -142 -117 rrcurveto + 9 -16 rlineto + 159 97 88 142 0 185 0 170 -91 167 -153 92 rrcurveto + endchar + + + -133 248 81 vstem + 86 1036 rmoveto + 90 -110 72 -169 0 -306 0 -303 -72 -172 -90 -110 rrcurveto + -30 vlineto + 142 134 101 214 0 267 0 272 -101 209 -142 134 rrcurveto + endchar + + + 7 383 95 vstem + 114 1530 rmoveto + 134 -165 135 -262 0 -460 0 -454 -135 -297 -134 -138 rrcurveto + -33 vlineto + 213 171 151 351 0 400 0 409 -151 314 -213 200 rrcurveto + endchar + + + 149 458 110 vstem + 83 2018 rmoveto + 178 -217 197 -351 0 -614 0 -606 -197 -396 -178 -184 rrcurveto + -44 vlineto + 284 228 201 468 0 534 0 545 -201 418 -284 267 rrcurveto + endchar + + + 207 554 130 vstem + 76 2510 rmoveto + 224 -232 254 -482 0 -766 0 -757 -254 -495 -224 -231 rrcurveto + -56 vlineto + 355 286 253 585 0 667 0 681 -253 570 -355 286 rrcurveto + endchar + 121 0 20 177 39 hstem 689 hmoveto @@ -245,6 +294,31 @@ 0 -303 89 -359 261 -184 rrcurveto endchar + + -151 276 124 vstem + 400 hmoveto + 159 vlineto + 0 303 -89 359 -261 184 rrcurveto + -30 vlineto + 172 -197 54 -285 0 -254 rrcurveto + -239 vlineto + endchar + + + -151 276 124 vstem + 400 hmoveto + 1010 -124 -1010 vlineto + endchar + + + -151 276 124 vstem + 400 1005 rmoveto + -124 -239 hlineto + 0 -254 -54 -285 -172 -197 rrcurveto + -30 vlineto + 261 184 89 359 0 303 rrcurveto + endchar + @@ -468,9 +542,13 @@ + + + + @@ -524,11 +602,12 @@ + - + @@ -580,6 +659,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -618,6 +747,11 @@ + + + + + @@ -629,6 +763,9 @@ + + + diff --git a/Tests/ttLib/data/duplicate_glyph_name.otf b/Tests/ttLib/data/duplicate_glyph_name.otf new file mode 100644 index 0000000000..66808c0179 Binary files /dev/null and b/Tests/ttLib/data/duplicate_glyph_name.otf differ diff --git a/Tests/ttLib/data/duplicate_glyph_name.ttf b/Tests/ttLib/data/duplicate_glyph_name.ttf new file mode 100644 index 0000000000..f7584c685b Binary files /dev/null and b/Tests/ttLib/data/duplicate_glyph_name.ttf differ diff --git a/Tests/ttLib/tables/T_S_I__0_test.py b/Tests/ttLib/tables/T_S_I__0_test.py index 871ece3d6e..dc739e1d4c 100644 --- a/Tests/ttLib/tables/T_S_I__0_test.py +++ b/Tests/ttLib/tables/T_S_I__0_test.py @@ -1,9 +1,9 @@ from types import SimpleNamespace -from fontTools.misc.textTools import deHexStr + +import pytest from fontTools.misc.testTools import getXML +from fontTools.misc.textTools import deHexStr from fontTools.ttLib.tables.T_S_I__0 import table_T_S_I__0 -import pytest - # (gid, length, offset) for glyph programs TSI0_INDICES = [(0, 1, 0), (1, 5, 1), (2, 0, 1), (3, 0, 1), (4, 8, 6)] @@ -71,6 +71,27 @@ def test_decompile(table, numGlyphs, data, expected_indices, expected_extra_indi assert table.extra_indices == expected_extra_indices +@pytest.mark.parametrize( + "numGlyphs, data, expected_indices, expected_extra_indices", + [ + (4, TSI0_DATA, TSI0_INDICES, TSI0_EXTRA_INDICES), + (6, TSI0_DATA, TSI0_INDICES, TSI0_EXTRA_INDICES), + ], + ids=["more entries than glyphs", "fewer entries than glyphs"], +) +def test_decompile_glyphs_mismatch( + table, numGlyphs, data, expected_indices, expected_extra_indices +): + font = {"maxp": SimpleNamespace(numGlyphs=numGlyphs)} + + table.decompile(data, font) + + assert len(table.indices) == 5 + assert table.indices == expected_indices + assert len(table.extra_indices) == 4 + assert table.extra_indices == expected_extra_indices + + @pytest.mark.parametrize( "numGlyphs, indices, extra_indices, expected_data", [ diff --git a/Tests/ttLib/tables/T_S_I__5_test.py b/Tests/ttLib/tables/T_S_I__5_test.py new file mode 100644 index 0000000000..cecaac2c3b --- /dev/null +++ b/Tests/ttLib/tables/T_S_I__5_test.py @@ -0,0 +1,77 @@ +from types import SimpleNamespace + +import pytest +from fontTools.misc.textTools import deHexStr +from fontTools.ttLib.tables.T_S_I__5 import table_T_S_I__5 + +# (type, length, offset) for 'extra' programs +TSI5_GLYPHGROUPS = { + ".notdef": 1, + "I": 1, + "space": 1, +} + +# compiled TSI5 table from data above +TSI5_DATA = deHexStr("0001 0001 0001") + + +class Font: + def __init__(self, numGlyphs: int) -> None: + self._numGlyphs = numGlyphs + self._glyphs = {0: ".notdef", 1: "I", 2: "space"} + self._tables = {"maxp": SimpleNamespace(numGlyphs=numGlyphs)} + + def __getitem__(self, key: str): + return self._tables[key] + + def getGlyphName(self, glyphID: int): + return self._glyphs.get(glyphID, f"glyph{glyphID:5d}") + + def getGlyphOrder(self): + return list(self._glyphs.values()) + + +@pytest.fixture +def table(): + return table_T_S_I__5() + + +@pytest.mark.parametrize( + "numGlyphs, data, expected_glyphgroups", + [ + (3, TSI5_DATA, TSI5_GLYPHGROUPS), + (4, TSI5_DATA, TSI5_GLYPHGROUPS), + (2, TSI5_DATA, TSI5_GLYPHGROUPS), + ], + ids=["simple", "fewer entries", "fewer glyphs"], +) +def test_decompile(table, numGlyphs, data, expected_glyphgroups): + font = Font(numGlyphs) + + table.decompile(data, font) + + assert len(table.glyphGrouping) == 3 + assert table.glyphGrouping == expected_glyphgroups + + +@pytest.mark.parametrize( + "numGlyphs, glyphgroups, expected_data", + [ + (3, TSI5_GLYPHGROUPS, TSI5_DATA), + (4, TSI5_GLYPHGROUPS, TSI5_DATA), + (2, TSI5_GLYPHGROUPS, TSI5_DATA), + ], + ids=["simple", "fewer entries", "fewer glyphs"], +) +def test_compile(table, numGlyphs, glyphgroups, expected_data): + # assert table.compile(ttFont=Font(numGlyphs)) == b"" + + table.glyphGrouping = TSI5_GLYPHGROUPS + data = table.compile(ttFont=Font(numGlyphs)) + assert data == expected_data + + +if __name__ == "__main__": + import sys + + sys.exit(pytest.main(sys.argv)) diff --git a/Tests/ttLib/tables/_c_v_t_test.py b/Tests/ttLib/tables/_c_v_t_test.py new file mode 100644 index 0000000000..b6d5f429ca --- /dev/null +++ b/Tests/ttLib/tables/_c_v_t_test.py @@ -0,0 +1,15 @@ +from fontTools.ttLib.tables._c_v_t import table__c_v_t + + +def test_compile_without_values(): + # When read from XML, in an empty cvt table, the values attribute is missing. + # Make sure that the table can be compiled to a zero-length table. + cvt = table__c_v_t() + assert cvt.compile(None) == b"" + + +def test_decompile_without_values(): + # VTT adds a zero-length table to a font if the Control Values are empty. + # Make sure it can be decompiled. + cvt = table__c_v_t() + cvt.decompile(b"", None) diff --git a/Tests/ttLib/tables/_f_p_g_m_test.py b/Tests/ttLib/tables/_f_p_g_m_test.py index ff233dd9cd..7900a112f5 100644 --- a/Tests/ttLib/tables/_f_p_g_m_test.py +++ b/Tests/ttLib/tables/_f_p_g_m_test.py @@ -1,5 +1,5 @@ -from fontTools.ttLib.tables._f_p_g_m import table__f_p_g_m from fontTools.ttLib.tables import ttProgram +from fontTools.ttLib.tables._f_p_g_m import table__f_p_g_m def test__bool__(): @@ -16,3 +16,17 @@ def test__bool__(): p.bytecode.pop() assert not bool(fpgm) + + +def test_compile_without_program(): + # When read from XML, in an empty fpgm table, the program attribute is missing. + # Make sure that the table can be compiled to a zero-length table. + fpgm = table__f_p_g_m() + assert fpgm.compile(None) == b"" + + +def test_decompile_without_program(): + # VTT adds a zero-length table to a font if the program is empty. + # Make sure it can be decompiled. + fpgm = table__f_p_g_m() + fpgm.decompile(b"", None) diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 10e053c4d5..9bc3c169f7 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -1,4 +1,5 @@ from fontTools.misc.fixedTools import otRound +from fontTools.misc.roundTools import noRound from fontTools.misc.testTools import getXML, parseXML from fontTools.misc.transform import Transform from fontTools.pens.ttGlyphPen import TTGlyphPen @@ -75,7 +76,6 @@ def test__neg__(self): g2 = -g assert g2 == GlyphCoordinates([(-1, -2)]) - @pytest.mark.skipif(sys.version_info[0] < 3, reason="__round___ requires Python 3") def test__round__(self): g = GlyphCoordinates([(-1.5, 2)]) g2 = round(g) @@ -538,6 +538,37 @@ def test_getCoordinates(self): ] ) + def test_get_components_nested(self): + # check that getCoordinates will correctly round with nested + glyphSet = {} + + pen = TTGlyphPen(glyphSet) + pen.moveTo((372, 736)) + pen.lineTo((68, 426)) + pen.qCurveTo((68.0, 426.0), (64.25, 414.0), (62.5, 386.25), (64.0, 372.0)) + pen.lineTo((0, 100)) + pen.lineTo((372, 736)) + pen.closePath() + + # turn off rounding here; we're pretending that this glyph has been + # through cubic->quadratic conversion, which introduced some floats + glyphSet["base"] = pen.glyph(round=noRound) + + pen = TTGlyphPen(glyphSet) + # this is a simple translation + pen.addComponent("base", (1, 0, 0, 1, 10, 10)) + glyphSet["simpleXform"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) + # this is flipped on the y axis + pen.addComponent("simpleXform", (-1, 0, 0, -1, 834, 793)) + glyphSet["nestedTrickyXForm"] = trickyXForm = pen.glyph() + + coords, _, _ = trickyXForm.getCoordinates(glyphSet, round=otRound) + assert all( + (int(p[0]), int(p[1])) == p for p in coords + ), f"{[p for p in coords]}" + def test_getCompositeMaxpValues(self): # https://github.com/fonttools/fonttools/issues/2044 glyphSet = {} @@ -567,17 +598,75 @@ def test_recalcBounds_empty_components(self): pen = TTGlyphPen(glyphSet) # empty simple glyph foo = glyphSet["foo"] = pen.glyph() + + pen.moveTo((5, 5)) + pen.lineTo((10, 10)) + pen.lineTo((10, 5)) + pen.lineTo((5, 5)) + pen.closePath() + + # non-empty simple glyph + doo = glyphSet["doo"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) # use the empty 'foo' glyph as a component in 'bar' with some x/y offsets pen.addComponent("foo", (1, 0, 0, 1, -80, 50)) + # and use the non-empty 'doo' glyph at origin + pen.addComponent("doo", (1, 0, 0, 1, 0, 0)) + bar = glyphSet["bar"] = pen.glyph() + + foo.recalcBounds(glyphSet) + doo.recalcBounds(glyphSet) + bar.recalcBounds(glyphSet) + + assert (foo.xMin, foo.yMin, foo.xMax, foo.yMax) == (0, 0, 0, 0) + assert (doo.xMin, doo.yMin, doo.xMax, doo.yMax) == (5, 5, 10, 10) + # the composite should have bounds identical to 'doo'; + # the empty foo glyph should do nothing + assert (bar.xMin, bar.yMin, bar.xMax, bar.yMax) == (5, 5, 10, 10) + + def test_recalcBounds_empty_components_nested(self): + # this differs from the above in an important way: it has a 'foofoo' + # glyph, which is a composite glyph, where none of its components + # have any contours; we want to check that when this glyph is referenced + # it is also treated as not contributing to the parent's bounds. + + glyphSet = {} + pen = TTGlyphPen(glyphSet) + # empty simple glyph + foo = glyphSet["foo"] = pen.glyph() + + pen.addComponent("foo", (1, 0, 0, 1, 20, 20)) + foofoo = glyphSet["foofoo"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) + + pen.moveTo((5, 5)) + pen.lineTo((10, 10)) + pen.lineTo((10, 5)) + pen.lineTo((5, 5)) + pen.closePath() + + # non-empty simple glyph + doo = glyphSet["doo"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) + # use the empty 'foo' glyph as a component in 'bar' with some x/y offsets + pen.addComponent("foofoo", (1, 0, 0, 1, 0, 0)) + # and use the non-empty 'doo' glyph at origin + pen.addComponent("doo", (1, 0, 0, 1, 0, 0)) bar = glyphSet["bar"] = pen.glyph() foo.recalcBounds(glyphSet) + foofoo.recalcBounds(glyphSet) + doo.recalcBounds(glyphSet) bar.recalcBounds(glyphSet) - # we expect both the empty simple glyph and the composite referencing it - # to have empty bounding boxes (0, 0, 0, 0) no matter the component's shift assert (foo.xMin, foo.yMin, foo.xMax, foo.yMax) == (0, 0, 0, 0) - assert (bar.xMin, bar.yMin, bar.xMax, bar.yMax) == (0, 0, 0, 0) + assert (doo.xMin, doo.yMin, doo.xMax, doo.yMax) == (5, 5, 10, 10) + # the composite should have bounds identical to 'doo'; + # the empty foo glyph should do nothing + assert (bar.xMin, bar.yMin, bar.xMax, bar.yMax) == (5, 5, 10, 10) class GlyphComponentTest: diff --git a/Tests/ttLib/tables/tables_test.py b/Tests/ttLib/tables/tables_test.py index 816385b64d..a20b545fa7 100644 --- a/Tests/ttLib/tables/tables_test.py +++ b/Tests/ttLib/tables/tables_test.py @@ -9,13 +9,9 @@ try: import unicodedata2 except ImportError: - if sys.version_info[:2] < (3, 6): - unicodedata2 = None - else: - # on 3.6 the built-in unicodedata is the same as unicodedata2 backport - import unicodedata + import unicodedata - unicodedata2 = unicodedata + unicodedata2 = unicodedata # Font files in data/*.{o,t}tf; output gets compared to data/*.ttx.* diff --git a/Tests/ttLib/ttFont_test.py b/Tests/ttLib/ttFont_test.py index 2203b4d9c3..033a2dc6bc 100644 --- a/Tests/ttLib/ttFont_test.py +++ b/Tests/ttLib/ttFont_test.py @@ -318,3 +318,21 @@ def seek(self, offset): f = UnsupportedSeekFile() with pytest.raises(TTLibError, match="Input file must be seekable when lazy=True"): TTFont(f, lazy=True) + + +@pytest.mark.parametrize( + "file_name", + [ + "duplicate_glyph_name.ttf", + "duplicate_glyph_name.otf", + ], +) +def test_duplicate_glyph_names(file_name): + font_path = os.path.join(DATA_DIR, file_name) + font = TTFont(font_path) + + assert font.getGlyphOrder() == [".notdef", "space", "A", "A.2", "A.1"] + + if "CFF " not in font: + post = font["post"] + assert post.mapping == {"A.2": "A"} diff --git a/Tests/varLib/builder_test.py b/Tests/varLib/builder_test.py index 33d1dfb023..0885ec746c 100644 --- a/Tests/varLib/builder_test.py +++ b/Tests/varLib/builder_test.py @@ -1,3 +1,13 @@ +from io import StringIO +from fontTools.designspaceLib import ( + AxisDescriptor, + DesignSpaceDocument, + SourceDescriptor, +) +from fontTools.fontBuilder import FontBuilder +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.varLib import build from fontTools.varLib.builder import buildVarData import pytest @@ -150,6 +160,87 @@ def test_buildVarData_optimize( assert data.Item == expected_items +def test_empty_vhvar_size(): + """HVAR/VHVAR should be present but empty when there are no glyph metrics + variations, and should use a direct mapping for optimal encoding.""" + + # Make a designspace that varies the outlines of 'A' but not its advance. + doc = DesignSpaceDocument() + + doc.addAxis( + AxisDescriptor(tag="wght", name="Weight", minimum=400, default=400, maximum=700) + ) + + for wght in (400, 700): + # Outlines depend on weight. + pen = TTGlyphPen(None) + pen.lineTo((0, wght)) + pen.lineTo((wght, wght)) + pen.lineTo((wght, 0)) + pen.closePath() + glyphs = {"A": pen.glyph()} + + fb = FontBuilder(unitsPerEm=1000) + fb.setupGlyphOrder(list(glyphs.keys())) + fb.setupGlyf(glyphs) + + # Horizontal advance does not vary. + fb.setupHorizontalMetrics( + {name: (500, fb.font["glyf"][name].xMin) for name in glyphs} # type: ignore + ) + fb.setupHorizontalHeader(ascent=1000, descent=0) + + # Vertical advance does not vary. + fb.setupVerticalMetrics( + {name: (500, 1000 - fb.font["glyf"][name].yMax) for name in glyphs} # type: ignore + ) + fb.setupVerticalHeader(ascent=1000, descent=0) + + fb.setupNameTable({"familyName": "TestEmptyVhvar", "styleName": "Regular"}) + fb.setupPost() + doc.addSource(SourceDescriptor(font=fb.font, location={"Weight": wght})) + + # Compile. + vf, *_ = build(doc) + + # Test both tables' encodings: + for table in ("HVAR", "VVAR"): + # Variable glyph metrics table should be built even when there are no + # glyph metrics variations. + assert table in vf + + # The table should be empty, and use a direct mapping for optimal size. + expected = """ + + + + + + + + + + + + + + + + +""".lstrip() + + with StringIO() as buffer: + writer = XMLWriter(buffer) + vf[table].toXML(writer, vf) + actual = buffer.getvalue() + assert actual == expected + + # The table should be encodable in at least this size. + # (VVAR has an extra Offset32 to point to a vertical origin mapping) + optimal_size = {"HVAR": 42, "VVAR": 46}[table] + assert len(vf[table].compile(vf)) <= optimal_size + + if __name__ == "__main__": import sys diff --git a/Tests/varLib/data/test_results/Build.ttx b/Tests/varLib/data/test_results/Build.ttx index f4fa0ae48e..c4f70e8aba 100644 --- a/Tests/varLib/data/test_results/Build.ttx +++ b/Tests/varLib/data/test_results/Build.ttx @@ -1,5 +1,5 @@ - + @@ -45,26 +45,19 @@ - + - - - - - + + + + + + - - - - - - - - diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx index 1c2c1fa8d2..c82323e425 100644 --- a/Tests/varLib/data/test_results/BuildMain.ttx +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -1,5 +1,5 @@ - + @@ -597,26 +597,19 @@ - + - - - - - + + + + + + - - - - - - - - diff --git a/Tests/varLib/interpolatable_test.py b/Tests/varLib/interpolatable_test.py index 92548b9e51..ac3038b1e6 100644 --- a/Tests/varLib/interpolatable_test.py +++ b/Tests/varLib/interpolatable_test.py @@ -94,9 +94,6 @@ def test_interpolatable_otf(self): otf_paths = self.get_file_list(self.tempdir, suffix) self.assertIsNone(interpolatable_main(otf_paths)) - @pytest.mark.skipif( - sys.version_info[:2] == (3, 8), reason="Fails on Python 3.8 for unknown reasons" - ) def test_interpolatable_cff2(self): suffix = ".otf" ttx_dir = self.get_test_input("variable_ttx_interpolatable_cff2") diff --git a/Tests/voltLib/data/NamdhinggoSIL1006.fea b/Tests/voltLib/data/NamdhinggoSIL1006.fea index aa8ab1a5de..575907316f 100644 --- a/Tests/voltLib/data/NamdhinggoSIL1006.fea +++ b/Tests/voltLib/data/NamdhinggoSIL1006.fea @@ -1,7 +1,7 @@ # Glyph classes @Cons = [uni1901 uni1902 uni1903 uni1904 uni1905 uni1906 uni1907 uni1908 uni1909 uni190A uni190B uni190C uni190D uni190E uni190F uni1910 uni1911 uni1912 uni1913 uni1914 uni1915 uni1916 uni1917 uni1918 uni1919 uni191A uni191B uni191C uni1940]; -@ConsRaU = [uni1901192A1922 uni1902192A1922 uni1903192A1922 uni1904192A1922 uni1905192A1922 uni1906192A1922 uni1907192A1922 uni1908192A1922 uni1909192A1922 uni190A192A1922 uni190B192A1922 uni190C192A1922 uni190D192A1922 uni190192AE1922 uni190F192A1922 uni1910192A1922 uni1911192A1922 uni1912192A1922 uni1913192A1922 uni1914192A1922 uni1915192A1922 uni1916192A1922 uni1917192A1922 uni1918192A1922 uni1919192A1922 uni1919192A1922 uni191A192A1922 uni191B192A1922 uni191C192A1922 uni1940192A1922]; @ConsU = [uni19011922 uni19021922 uni19031922 uni19041922 uni19051922 uni19061922 uni19071922 uni19081922 uni19091922 uni190A1922 uni190B1922 uni190C1922 uni190D1922 uni190E1922 uni190F1922 uni19101922 uni19111922 uni19121922 uni19131922 uni19141922 uni19151922 uni19161922 uni19171922 uni19181922 uni19191922 uni191A1922 uni191B1922 uni191C1922 uni19401922]; +@ConsRaU = [uni1901192A1922 uni1902192A1922 uni1903192A1922 uni1904192A1922 uni1905192A1922 uni1906192A1922 uni1907192A1922 uni1908192A1922 uni1909192A1922 uni190A192A1922 uni190B192A1922 uni190C192A1922 uni190D192A1922 uni190192AE1922 uni190F192A1922 uni1910192A1922 uni1911192A1922 uni1912192A1922 uni1913192A1922 uni1914192A1922 uni1915192A1922 uni1916192A1922 uni1917192A1922 uni1918192A1922 uni1919192A1922 uni1919192A1922 uni191A192A1922 uni191B192A1922 uni191C192A1922 uni1940192A1922]; @Ikar = [uni1921 uni1921193A]; @Vowels = [uni1920 uni1927 uni1928]; @YaWa = [uni1929 uni192B]; @@ -9,12 +9,12 @@ @VowelsKem = [@Vowels uni193A]; # Mark classes -markClass uni1920 @Aabove; -markClass uni1922 @U; -markClass uni1927 @eo; -markClass uni1928 @eo; -markClass uni193A @K; -markClass uni193A @VK; +markClass uni1920 @Aabove.Akar; +markClass uni1922 @U.GlideU; +markClass uni1927 @eo.EO; +markClass uni1928 @eo.EO; +markClass uni193A @K.Kemphreng; +markClass uni193A @VK.VKem; # Lookups lookup EEAIDecomp { @@ -77,12 +77,70 @@ lookup GlideVowelDecomp { lookup RaUkar { # The RaUkar substitution rule replaces Consonant, Ra, Ukar with a ligature. - sub @Cons uni192A uni1922 by @ConsRaU; + # sub @Cons uni192A uni1922 by @ConsRaU; + sub uni1901 uni192A uni1922 by uni1901192A1922; + sub uni1902 uni192A uni1922 by uni1902192A1922; + sub uni1903 uni192A uni1922 by uni1903192A1922; + sub uni1904 uni192A uni1922 by uni1904192A1922; + sub uni1905 uni192A uni1922 by uni1905192A1922; + sub uni1906 uni192A uni1922 by uni1906192A1922; + sub uni1907 uni192A uni1922 by uni1907192A1922; + sub uni1908 uni192A uni1922 by uni1908192A1922; + sub uni1909 uni192A uni1922 by uni1909192A1922; + sub uni190A uni192A uni1922 by uni190A192A1922; + sub uni190B uni192A uni1922 by uni190B192A1922; + sub uni190C uni192A uni1922 by uni190C192A1922; + sub uni190D uni192A uni1922 by uni190D192A1922; + sub uni190E uni192A uni1922 by uni190192AE1922; + sub uni190F uni192A uni1922 by uni190F192A1922; + sub uni1910 uni192A uni1922 by uni1910192A1922; + sub uni1911 uni192A uni1922 by uni1911192A1922; + sub uni1912 uni192A uni1922 by uni1912192A1922; + sub uni1913 uni192A uni1922 by uni1913192A1922; + sub uni1914 uni192A uni1922 by uni1914192A1922; + sub uni1915 uni192A uni1922 by uni1915192A1922; + sub uni1916 uni192A uni1922 by uni1916192A1922; + sub uni1917 uni192A uni1922 by uni1917192A1922; + sub uni1918 uni192A uni1922 by uni1918192A1922; + sub uni1919 uni192A uni1922 by uni1919192A1922; + sub uni191A uni192A uni1922 by uni1919192A1922; + sub uni191B uni192A uni1922 by uni191A192A1922; + sub uni191C uni192A uni1922 by uni191B192A1922; + sub uni1940 uni192A uni1922 by uni191C192A1922; } RaUkar; lookup Ukar { # The Ukar substitution rule replaces Consonant + Ukar with a ligature. It also applies to the Vowel-Carrier, which has its own ligature with ukar. - sub @Cons uni1922 by @ConsU; + # sub @Cons uni1922 by @ConsU; + sub uni1901 uni1922 by uni19011922; + sub uni1902 uni1922 by uni19021922; + sub uni1903 uni1922 by uni19031922; + sub uni1904 uni1922 by uni19041922; + sub uni1905 uni1922 by uni19051922; + sub uni1906 uni1922 by uni19061922; + sub uni1907 uni1922 by uni19071922; + sub uni1908 uni1922 by uni19081922; + sub uni1909 uni1922 by uni19091922; + sub uni190A uni1922 by uni190A1922; + sub uni190B uni1922 by uni190B1922; + sub uni190C uni1922 by uni190C1922; + sub uni190D uni1922 by uni190D1922; + sub uni190E uni1922 by uni190E1922; + sub uni190F uni1922 by uni190F1922; + sub uni1910 uni1922 by uni19101922; + sub uni1911 uni1922 by uni19111922; + sub uni1912 uni1922 by uni19121922; + sub uni1913 uni1922 by uni19131922; + sub uni1914 uni1922 by uni19141922; + sub uni1915 uni1922 by uni19151922; + sub uni1916 uni1922 by uni19161922; + sub uni1917 uni1922 by uni19171922; + sub uni1918 uni1922 by uni19181922; + sub uni1919 uni1922 by uni19191922; + sub uni191A uni1922 by uni191A1922; + sub uni191B uni1922 by uni191B1922; + sub uni191C uni1922 by uni191C1922; + sub uni1940 uni1922 by uni19401922; sub uni1900 uni1922 by uni19001922; } Ukar; @@ -91,346 +149,346 @@ lookup IkarK { sub uni1921 uni193A by uni1921193A; } IkarK; -lookup GlideIkar_target { +lookup GlideIkar_chained { pos @YaWa -475; -} GlideIkar_target; +} GlideIkar_chained; lookup GlideIkar { - pos [@YaWa]' lookup GlideIkar_target @Ikar; + pos [@YaWa]' lookup GlideIkar_chained @Ikar; } GlideIkar; -lookup IkarKWid_target { +lookup IkarKWid_chained { pos uni1921193A 110; -} IkarKWid_target; +} IkarKWid_chained; lookup IkarKWid { # The IkarKWid lookup, applied to the Kern feature, adds 110 units of width to the IkarKemphreng ligature when followed by a consonant with akar on it. This prevents the akar from overprinting the rightmost dot of the kemphreng. (The dot overhangs to the right slightly, which is OK unless the following character has akar on it). - pos [uni1921193A]' lookup IkarKWid_target @Cons uni1920; + pos [uni1921193A]' lookup IkarKWid_chained @Cons uni1920; } IkarKWid; lookup Akar { # The Akar positioning rule positions the Akar on all consonants. pos base uni1901 - mark @Aabove; + mark @Aabove.Akar; pos base uni1902 - mark @Aabove; + mark @Aabove.Akar; pos base uni1903 - mark @Aabove; + mark @Aabove.Akar; pos base uni1904 - mark @Aabove; + mark @Aabove.Akar; pos base uni1905 - mark @Aabove; + mark @Aabove.Akar; pos base uni1906 - mark @Aabove; + mark @Aabove.Akar; pos base uni1907 - mark @Aabove; + mark @Aabove.Akar; pos base uni1908 - mark @Aabove; + mark @Aabove.Akar; pos base uni1909 - mark @Aabove; + mark @Aabove.Akar; pos base uni190A - mark @Aabove; + mark @Aabove.Akar; pos base uni190B - mark @Aabove; + mark @Aabove.Akar; pos base uni190C - mark @Aabove; + mark @Aabove.Akar; pos base uni190D - mark @Aabove; + mark @Aabove.Akar; pos base uni190E - mark @Aabove; + mark @Aabove.Akar; pos base uni190F - mark @Aabove; + mark @Aabove.Akar; pos base uni1910 - mark @Aabove; + mark @Aabove.Akar; pos base uni1911 - mark @Aabove; + mark @Aabove.Akar; pos base uni1912 - mark @Aabove; + mark @Aabove.Akar; pos base uni1913 - mark @Aabove; + mark @Aabove.Akar; pos base uni1914 - mark @Aabove; + mark @Aabove.Akar; pos base uni1915 - mark @Aabove; + mark @Aabove.Akar; pos base uni1916 - mark @Aabove; + mark @Aabove.Akar; pos base uni1917 - mark @Aabove; + mark @Aabove.Akar; pos base uni1918 - mark @Aabove; + mark @Aabove.Akar; pos base uni1919 - mark @Aabove; + mark @Aabove.Akar; pos base uni191A - mark @Aabove; + mark @Aabove.Akar; pos base uni191B - mark @Aabove; + mark @Aabove.Akar; pos base uni191C - mark @Aabove; + mark @Aabove.Akar; pos base uni1940 - mark @Aabove; + mark @Aabove.Akar; } Akar; lookup Kemphreng { # The Kemphreng positioning rule positions the Kemphreng on all consonants, including the vowel carrier. pos base uni1901 - mark @K; + mark @K.Kemphreng; pos base uni1902 - mark @K; + mark @K.Kemphreng; pos base uni1903 - mark @K; + mark @K.Kemphreng; pos base uni1904 - mark @K; + mark @K.Kemphreng; pos base uni1905 - mark @K; + mark @K.Kemphreng; pos base uni1906 - mark @K; + mark @K.Kemphreng; pos base uni1907 - mark @K; + mark @K.Kemphreng; pos base uni1908 - mark @K; + mark @K.Kemphreng; pos base uni1909 - mark @K; + mark @K.Kemphreng; pos base uni190A - mark @K; + mark @K.Kemphreng; pos base uni190B - mark @K; + mark @K.Kemphreng; pos base uni190C - mark @K; + mark @K.Kemphreng; pos base uni190D - mark @K; + mark @K.Kemphreng; pos base uni190E - mark @K; + mark @K.Kemphreng; pos base uni190F - mark @K; + mark @K.Kemphreng; pos base uni1910 - mark @K; + mark @K.Kemphreng; pos base uni1911 - mark @K; + mark @K.Kemphreng; pos base uni1912 - mark @K; + mark @K.Kemphreng; pos base uni1913 - mark @K; + mark @K.Kemphreng; pos base uni1914 - mark @K; + mark @K.Kemphreng; pos base uni1915 - mark @K; + mark @K.Kemphreng; pos base uni1916 - mark @K; + mark @K.Kemphreng; pos base uni1917 - mark @K; + mark @K.Kemphreng; pos base uni1918 - mark @K; + mark @K.Kemphreng; pos base uni1919 - mark @K; + mark @K.Kemphreng; pos base uni191A - mark @K; + mark @K.Kemphreng; pos base uni191B - mark @K; + mark @K.Kemphreng; pos base uni191C - mark @K; + mark @K.Kemphreng; pos base uni1940 - mark @K; + mark @K.Kemphreng; pos base uni19011922 - mark @K; + mark @K.Kemphreng; pos base uni19021922 - mark @K; + mark @K.Kemphreng; pos base uni19031922 - mark @K; + mark @K.Kemphreng; pos base uni19041922 - mark @K; + mark @K.Kemphreng; pos base uni19051922 - mark @K; + mark @K.Kemphreng; pos base uni19061922 - mark @K; + mark @K.Kemphreng; pos base uni19071922 - mark @K; + mark @K.Kemphreng; pos base uni19081922 - mark @K; + mark @K.Kemphreng; pos base uni19091922 - mark @K; + mark @K.Kemphreng; pos base uni190A1922 - mark @K; + mark @K.Kemphreng; pos base uni190B1922 - mark @K; + mark @K.Kemphreng; pos base uni190C1922 - mark @K; + mark @K.Kemphreng; pos base uni190D1922 - mark @K; + mark @K.Kemphreng; pos base uni190E1922 - mark @K; + mark @K.Kemphreng; pos base uni190F1922 - mark @K; + mark @K.Kemphreng; pos base uni19101922 - mark @K; + mark @K.Kemphreng; pos base uni19111922 - mark @K; + mark @K.Kemphreng; pos base uni19121922 - mark @K; + mark @K.Kemphreng; pos base uni19131922 - mark @K; + mark @K.Kemphreng; pos base uni19141922 - mark @K; + mark @K.Kemphreng; pos base uni19151922 - mark @K; + mark @K.Kemphreng; pos base uni19161922 - mark @K; + mark @K.Kemphreng; pos base uni19171922 - mark @K; + mark @K.Kemphreng; pos base uni19181922 - mark @K; + mark @K.Kemphreng; pos base uni19191922 - mark @K; + mark @K.Kemphreng; pos base uni191A1922 - mark @K; + mark @K.Kemphreng; pos base uni191B1922 - mark @K; + mark @K.Kemphreng; pos base uni191C1922 - mark @K; + mark @K.Kemphreng; pos base uni19401922 - mark @K; + mark @K.Kemphreng; pos base uni1901192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1902192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1903192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1904192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1905192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1906192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1907192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1908192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1909192A1922 - mark @K; + mark @K.Kemphreng; pos base uni190A192A1922 - mark @K; + mark @K.Kemphreng; pos base uni190B192A1922 - mark @K; + mark @K.Kemphreng; pos base uni190C192A1922 - mark @K; + mark @K.Kemphreng; pos base uni190D192A1922 - mark @K; + mark @K.Kemphreng; pos base uni190192AE1922 - mark @K; + mark @K.Kemphreng; pos base uni190F192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1910192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1911192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1912192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1913192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1914192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1915192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1916192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1917192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1918192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1919192A1922 - mark @K; + mark @K.Kemphreng; pos base uni191A192A1922 - mark @K; + mark @K.Kemphreng; pos base uni191B192A1922 - mark @K; + mark @K.Kemphreng; pos base uni191C192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1940192A1922 - mark @K; + mark @K.Kemphreng; pos base uni1900 - mark @K; + mark @K.Kemphreng; } Kemphreng; lookup EO { # The IEO positioning rule positions ikar (including the ligature with kemphreng), e and o on all consonants plus the vowel carrier. pos base uni1901 - mark @eo; + mark @eo.EO; pos base uni1902 - mark @eo; + mark @eo.EO; pos base uni1903 - mark @eo; + mark @eo.EO; pos base uni1904 - mark @eo; + mark @eo.EO; pos base uni1905 - mark @eo; + mark @eo.EO; pos base uni1906 - mark @eo; + mark @eo.EO; pos base uni1907 - mark @eo; + mark @eo.EO; pos base uni1908 - mark @eo; + mark @eo.EO; pos base uni1909 - mark @eo; + mark @eo.EO; pos base uni190A - mark @eo; + mark @eo.EO; pos base uni190B - mark @eo; + mark @eo.EO; pos base uni190C - mark @eo; + mark @eo.EO; pos base uni190D - mark @eo; + mark @eo.EO; pos base uni190E - mark @eo; + mark @eo.EO; pos base uni190F - mark @eo; + mark @eo.EO; pos base uni1910 - mark @eo; + mark @eo.EO; pos base uni1911 - mark @eo; + mark @eo.EO; pos base uni1912 - mark @eo; + mark @eo.EO; pos base uni1913 - mark @eo; + mark @eo.EO; pos base uni1914 - mark @eo; + mark @eo.EO; pos base uni1915 - mark @eo; + mark @eo.EO; pos base uni1916 - mark @eo; + mark @eo.EO; pos base uni1917 - mark @eo; + mark @eo.EO; pos base uni1918 - mark @eo; + mark @eo.EO; pos base uni1919 - mark @eo; + mark @eo.EO; pos base uni191A - mark @eo; + mark @eo.EO; pos base uni191B - mark @eo; + mark @eo.EO; pos base uni191C - mark @eo; + mark @eo.EO; pos base uni1940 - mark @eo; + mark @eo.EO; pos base uni1900 - mark @eo; + mark @eo.EO; } EO; lookup VKem { lookupflag MarkAttachmentType @VowelsKem; # The VKem positioning rule positions the kemphreng on all upper vowels (except ikar, which has its own ligature). The vowel itself is positioned on the consonant with the Akar or IEO positioning rule. pos mark uni1920 - mark @VK; + mark @VK.VKem; pos mark uni1927 - mark @VK; + mark @VK.VKem; pos mark uni1928 - mark @VK; + mark @VK.VKem; } VKem; lookup GlideU { # The GlideU positioning rule positions the ukar on the glides Ya and Wa. (There is already a ligature for each consonant with the Ra+Ukar combination). pos base uni1929 - mark @U; + mark @U.GlideU; pos base uni192B - mark @U; + mark @U.GlideU; } GlideU; # Features diff --git a/Tests/voltLib/data/Nutso.fea b/Tests/voltLib/data/Nutso.fea index 7a2c44bbfc..bd5339e0e7 100644 --- a/Tests/voltLib/data/Nutso.fea +++ b/Tests/voltLib/data/Nutso.fea @@ -5,124 +5,152 @@ @slash = [slash fraction]; # Mark classes -markClass eight.numr @INIT.1.10; -markClass eight.numr @INIT.2.10; -markClass eight.numr @INIT.3.10; -markClass eight.numr @INIT.4.10; -markClass eight.numr @INIT.5.10; -markClass eight.numr @INIT.6.10; -markClass eight.numr @INIT.7.10; -markClass eight.numr @INIT.8.10; -markClass eight.numr @INIT.9.10; -markClass eight.numr @NUMRNUMR; -markClass five.numr @INIT.1.10; -markClass five.numr @INIT.2.10; -markClass five.numr @INIT.3.10; -markClass five.numr @INIT.4.10; -markClass five.numr @INIT.5.10; -markClass five.numr @INIT.6.10; -markClass five.numr @INIT.7.10; -markClass five.numr @INIT.8.10; -markClass five.numr @INIT.9.10; -markClass five.numr @NUMRNUMR; -markClass four.numr @INIT.1.10; -markClass four.numr @INIT.2.10; -markClass four.numr @INIT.3.10; -markClass four.numr @INIT.4.10; -markClass four.numr @INIT.5.10; -markClass four.numr @INIT.6.10; -markClass four.numr @INIT.7.10; -markClass four.numr @INIT.8.10; -markClass four.numr @INIT.9.10; -markClass four.numr @NUMRNUMR; -markClass nine.numr @INIT.1.10; -markClass nine.numr @INIT.2.10; -markClass nine.numr @INIT.3.10; -markClass nine.numr @INIT.4.10; -markClass nine.numr @INIT.5.10; -markClass nine.numr @INIT.6.10; -markClass nine.numr @INIT.7.10; -markClass nine.numr @INIT.8.10; -markClass nine.numr @INIT.9.10; -markClass nine.numr @NUMRNUMR; -markClass one.numr @INIT.1.10; -markClass one.numr @INIT.2.10; -markClass one.numr @INIT.3.10; -markClass one.numr @INIT.4.10; -markClass one.numr @INIT.5.10; -markClass one.numr @INIT.6.10; -markClass one.numr @INIT.7.10; -markClass one.numr @INIT.8.10; -markClass one.numr @INIT.9.10; -markClass one.numr @NUMRNUMR; -markClass seven.numr @INIT.1.10; -markClass seven.numr @INIT.2.10; -markClass seven.numr @INIT.3.10; -markClass seven.numr @INIT.4.10; -markClass seven.numr @INIT.5.10; -markClass seven.numr @INIT.6.10; -markClass seven.numr @INIT.7.10; -markClass seven.numr @INIT.8.10; -markClass seven.numr @INIT.9.10; -markClass seven.numr @NUMRNUMR; -markClass six.numr @INIT.1.10; -markClass six.numr @INIT.2.10; -markClass six.numr @INIT.3.10; -markClass six.numr @INIT.4.10; -markClass six.numr @INIT.5.10; -markClass six.numr @INIT.6.10; -markClass six.numr @INIT.7.10; -markClass six.numr @INIT.8.10; -markClass six.numr @INIT.9.10; -markClass six.numr @NUMRNUMR; -markClass three.numr @INIT.1.10; -markClass three.numr @INIT.2.10; -markClass three.numr @INIT.3.10; -markClass three.numr @INIT.4.10; -markClass three.numr @INIT.5.10; -markClass three.numr @INIT.6.10; -markClass three.numr @INIT.7.10; -markClass three.numr @INIT.8.10; -markClass three.numr @INIT.9.10; -markClass three.numr @NUMRNUMR; -markClass two.numr @INIT.1.10; -markClass two.numr @INIT.2.10; -markClass two.numr @INIT.3.10; -markClass two.numr @INIT.4.10; -markClass two.numr @INIT.5.10; -markClass two.numr @INIT.6.10; -markClass two.numr @INIT.7.10; -markClass two.numr @INIT.8.10; -markClass two.numr @INIT.9.10; -markClass two.numr @NUMRNUMR; -markClass zero.numr @INIT.1.10; -markClass zero.numr @INIT.2.10; -markClass zero.numr @INIT.3.10; -markClass zero.numr @INIT.4.10; -markClass zero.numr @INIT.5.10; -markClass zero.numr @INIT.6.10; -markClass zero.numr @INIT.7.10; -markClass zero.numr @INIT.8.10; -markClass zero.numr @INIT.9.10; -markClass zero.numr @NUMRNUMR; +markClass eight.numr @INIT.1.10.fracmark.init_1.10; +markClass eight.numr @INIT.2.10.fracmark.init_2.10; +markClass eight.numr @INIT.3.10.fracmark.init_3.10; +markClass eight.numr @INIT.4.10.fracmark.init_4.10; +markClass eight.numr @INIT.5.10.fracmark.init_5.10; +markClass eight.numr @INIT.6.10.fracmark.init_6.10; +markClass eight.numr @INIT.7.10.fracmark.init_7.10; +markClass eight.numr @INIT.8.10.fracmark.init_8.10; +markClass eight.numr @INIT.9.10.fracmark.init_9.10; +markClass eight.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass five.numr @INIT.1.10.fracmark.init_1.10; +markClass five.numr @INIT.2.10.fracmark.init_2.10; +markClass five.numr @INIT.3.10.fracmark.init_3.10; +markClass five.numr @INIT.4.10.fracmark.init_4.10; +markClass five.numr @INIT.5.10.fracmark.init_5.10; +markClass five.numr @INIT.6.10.fracmark.init_6.10; +markClass five.numr @INIT.7.10.fracmark.init_7.10; +markClass five.numr @INIT.8.10.fracmark.init_8.10; +markClass five.numr @INIT.9.10.fracmark.init_9.10; +markClass five.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass four.numr @INIT.1.10.fracmark.init_1.10; +markClass four.numr @INIT.2.10.fracmark.init_2.10; +markClass four.numr @INIT.3.10.fracmark.init_3.10; +markClass four.numr @INIT.4.10.fracmark.init_4.10; +markClass four.numr @INIT.5.10.fracmark.init_5.10; +markClass four.numr @INIT.6.10.fracmark.init_6.10; +markClass four.numr @INIT.7.10.fracmark.init_7.10; +markClass four.numr @INIT.8.10.fracmark.init_8.10; +markClass four.numr @INIT.9.10.fracmark.init_9.10; +markClass four.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass nine.numr @INIT.1.10.fracmark.init_1.10; +markClass nine.numr @INIT.2.10.fracmark.init_2.10; +markClass nine.numr @INIT.3.10.fracmark.init_3.10; +markClass nine.numr @INIT.4.10.fracmark.init_4.10; +markClass nine.numr @INIT.5.10.fracmark.init_5.10; +markClass nine.numr @INIT.6.10.fracmark.init_6.10; +markClass nine.numr @INIT.7.10.fracmark.init_7.10; +markClass nine.numr @INIT.8.10.fracmark.init_8.10; +markClass nine.numr @INIT.9.10.fracmark.init_9.10; +markClass nine.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass one.numr @INIT.1.10.fracmark.init_1.10; +markClass one.numr @INIT.2.10.fracmark.init_2.10; +markClass one.numr @INIT.3.10.fracmark.init_3.10; +markClass one.numr @INIT.4.10.fracmark.init_4.10; +markClass one.numr @INIT.5.10.fracmark.init_5.10; +markClass one.numr @INIT.6.10.fracmark.init_6.10; +markClass one.numr @INIT.7.10.fracmark.init_7.10; +markClass one.numr @INIT.8.10.fracmark.init_8.10; +markClass one.numr @INIT.9.10.fracmark.init_9.10; +markClass one.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass seven.numr @INIT.1.10.fracmark.init_1.10; +markClass seven.numr @INIT.2.10.fracmark.init_2.10; +markClass seven.numr @INIT.3.10.fracmark.init_3.10; +markClass seven.numr @INIT.4.10.fracmark.init_4.10; +markClass seven.numr @INIT.5.10.fracmark.init_5.10; +markClass seven.numr @INIT.6.10.fracmark.init_6.10; +markClass seven.numr @INIT.7.10.fracmark.init_7.10; +markClass seven.numr @INIT.8.10.fracmark.init_8.10; +markClass seven.numr @INIT.9.10.fracmark.init_9.10; +markClass seven.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass six.numr @INIT.1.10.fracmark.init_1.10; +markClass six.numr @INIT.2.10.fracmark.init_2.10; +markClass six.numr @INIT.3.10.fracmark.init_3.10; +markClass six.numr @INIT.4.10.fracmark.init_4.10; +markClass six.numr @INIT.5.10.fracmark.init_5.10; +markClass six.numr @INIT.6.10.fracmark.init_6.10; +markClass six.numr @INIT.7.10.fracmark.init_7.10; +markClass six.numr @INIT.8.10.fracmark.init_8.10; +markClass six.numr @INIT.9.10.fracmark.init_9.10; +markClass six.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass three.numr @INIT.1.10.fracmark.init_1.10; +markClass three.numr @INIT.2.10.fracmark.init_2.10; +markClass three.numr @INIT.3.10.fracmark.init_3.10; +markClass three.numr @INIT.4.10.fracmark.init_4.10; +markClass three.numr @INIT.5.10.fracmark.init_5.10; +markClass three.numr @INIT.6.10.fracmark.init_6.10; +markClass three.numr @INIT.7.10.fracmark.init_7.10; +markClass three.numr @INIT.8.10.fracmark.init_8.10; +markClass three.numr @INIT.9.10.fracmark.init_9.10; +markClass three.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass two.numr @INIT.1.10.fracmark.init_1.10; +markClass two.numr @INIT.2.10.fracmark.init_2.10; +markClass two.numr @INIT.3.10.fracmark.init_3.10; +markClass two.numr @INIT.4.10.fracmark.init_4.10; +markClass two.numr @INIT.5.10.fracmark.init_5.10; +markClass two.numr @INIT.6.10.fracmark.init_6.10; +markClass two.numr @INIT.7.10.fracmark.init_7.10; +markClass two.numr @INIT.8.10.fracmark.init_8.10; +markClass two.numr @INIT.9.10.fracmark.init_9.10; +markClass two.numr @NUMRNUMR.fracmkmk.numrspacing; +markClass zero.numr @INIT.1.10.fracmark.init_1.10; +markClass zero.numr @INIT.2.10.fracmark.init_2.10; +markClass zero.numr @INIT.3.10.fracmark.init_3.10; +markClass zero.numr @INIT.4.10.fracmark.init_4.10; +markClass zero.numr @INIT.5.10.fracmark.init_5.10; +markClass zero.numr @INIT.6.10.fracmark.init_6.10; +markClass zero.numr @INIT.7.10.fracmark.init_7.10; +markClass zero.numr @INIT.8.10.fracmark.init_8.10; +markClass zero.numr @INIT.9.10.fracmark.init_9.10; +markClass zero.numr @NUMRNUMR.fracmkmk.numrspacing; # Lookups lookup frac.numr { sub @numerals by @numr; } frac.numr; +lookup frac.dnom_chained { + sub @numr by @dnom; +} frac.dnom_chained; + lookup frac.dnom { - sub [@slash @dnom] @numr' by @dnom; + sub [@slash @dnom] @numr' lookup frac.dnom_chained; } frac.dnom; lookup frac.noslash { - sub @numr slash by @numr; - sub @numr fraction by @numr; + # sub @numr slash by @numr; + sub zero.numr slash by zero.numr; + sub one.numr slash by one.numr; + sub two.numr slash by two.numr; + sub three.numr slash by three.numr; + sub four.numr slash by four.numr; + sub five.numr slash by five.numr; + sub six.numr slash by six.numr; + sub seven.numr slash by seven.numr; + sub eight.numr slash by eight.numr; + sub nine.numr slash by nine.numr; + # sub @numr fraction by @numr; + sub zero.numr fraction by zero.numr; + sub one.numr fraction by one.numr; + sub two.numr fraction by two.numr; + sub three.numr fraction by three.numr; + sub four.numr fraction by four.numr; + sub five.numr fraction by five.numr; + sub six.numr fraction by six.numr; + sub seven.numr fraction by seven.numr; + sub eight.numr fraction by eight.numr; + sub nine.numr fraction by nine.numr; } frac.noslash; +lookup frac.fracinit_chained { + sub @numr by fracinit @numr; +} frac.fracinit_chained; + lookup frac.fracinit { ignore sub @numr @numr'; - sub @numr' by fracinit @numr; + sub @numr' lookup frac.fracinit_chained; } frac.fracinit; lookup kern.numeral_to_fraction { @@ -130,137 +158,137 @@ lookup kern.numeral_to_fraction { pos @dnom @numerals 140; } kern.numeral_to_fraction; -lookup fracmark.init_1.10_target { +lookup fracmark.init_1.10_chained { pos base fracinit - mark @INIT.1.10; -} fracmark.init_1.10_target; + mark @INIT.1.10.fracmark.init_1.10; +} fracmark.init_1.10_chained; -lookup fracmark.init_2.10_target { +lookup fracmark.init_2.10_chained { pos base fracinit - mark @INIT.2.10; -} fracmark.init_2.10_target; + mark @INIT.2.10.fracmark.init_2.10; +} fracmark.init_2.10_chained; -lookup fracmark.init_3.10_target { +lookup fracmark.init_3.10_chained { pos base fracinit - mark @INIT.3.10; -} fracmark.init_3.10_target; + mark @INIT.3.10.fracmark.init_3.10; +} fracmark.init_3.10_chained; -lookup fracmark.init_4.10_target { +lookup fracmark.init_4.10_chained { pos base fracinit - mark @INIT.4.10; -} fracmark.init_4.10_target; + mark @INIT.4.10.fracmark.init_4.10; +} fracmark.init_4.10_chained; -lookup fracmark.init_5.10_target { +lookup fracmark.init_5.10_chained { pos base fracinit - mark @INIT.5.10; -} fracmark.init_5.10_target; + mark @INIT.5.10.fracmark.init_5.10; +} fracmark.init_5.10_chained; -lookup fracmark.init_6.10_target { +lookup fracmark.init_6.10_chained { pos base fracinit - mark @INIT.6.10; -} fracmark.init_6.10_target; + mark @INIT.6.10.fracmark.init_6.10; +} fracmark.init_6.10_chained; -lookup fracmark.init_7.10_target { +lookup fracmark.init_7.10_chained { pos base fracinit - mark @INIT.7.10; -} fracmark.init_7.10_target; + mark @INIT.7.10.fracmark.init_7.10; +} fracmark.init_7.10_chained; -lookup fracmark.init_8.10_target { +lookup fracmark.init_8.10_chained { pos base fracinit - mark @INIT.8.10; -} fracmark.init_8.10_target; + mark @INIT.8.10.fracmark.init_8.10; +} fracmark.init_8.10_chained; -lookup fracmark.init_9.10_target { +lookup fracmark.init_9.10_chained { pos base fracinit - mark @INIT.9.10; -} fracmark.init_9.10_target; + mark @INIT.9.10.fracmark.init_9.10; +} fracmark.init_9.10_chained; lookup fracmark.init { # fracmark.init\1.10 - pos [@numr]' lookup fracmark.init_1.10_target @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_1.10_chained @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; subtable; # fracmark.init\2.10 - pos [@numr]' lookup fracmark.init_2.10_target @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_2.10_target @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_2.10_chained @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_2.10_chained @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; subtable; # fracmark.init\3.10 - pos [@numr]' lookup fracmark.init_3.10_target @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_3.10_target @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_3.10_target @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_3.10_chained @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_3.10_chained @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_3.10_chained @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; subtable; # fracmark.init\4.10 - pos [@numr]' lookup fracmark.init_4.10_target @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_4.10_target @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_4.10_target @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_4.10_target @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_4.10_chained @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_4.10_chained @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_4.10_chained @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_4.10_chained @dnom @dnom @dnom @dnom @dnom @dnom @dnom; subtable; # fracmark.init\5.10 - pos [@numr]' lookup fracmark.init_5.10_target @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_5.10_target @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_5.10_target @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_5.10_target @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_5.10_target @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_5.10_chained @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_5.10_chained @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_5.10_chained @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_5.10_chained @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_5.10_chained @dnom @dnom @dnom @dnom @dnom @dnom; subtable; # fracmark.init\6.10 - pos [@numr]' lookup fracmark.init_6.10_target @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_6.10_target @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_6.10_target @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_6.10_target @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_6.10_target @numr @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_6.10_target @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_6.10_chained @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_6.10_chained @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_6.10_chained @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_6.10_chained @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_6.10_chained @numr @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_6.10_chained @dnom @dnom @dnom @dnom @dnom; subtable; # fracmark.init\7.10 - pos [@numr]' lookup fracmark.init_7.10_target @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_7.10_target @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_7.10_target @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_7.10_target @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_7.10_target @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_7.10_target @numr @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_7.10_target @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @numr @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_7.10_chained @dnom @dnom @dnom @dnom; subtable; # fracmark.init\8.10 - pos [@numr]' lookup fracmark.init_8.10_target @numr @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @numr @numr @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @numr @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_8.10_target @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @numr @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @numr @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_8.10_chained @dnom @dnom @dnom; subtable; # fracmark.init\9.10 - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @numr @dnom @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @numr @dnom @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @numr @dnom @dnom @dnom; - pos [@numr]' lookup fracmark.init_9.10_target @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @numr @numr @dnom @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @numr @dnom @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @numr @dnom @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @numr @dnom @dnom @dnom; + pos [@numr]' lookup fracmark.init_9.10_chained @dnom @dnom; } fracmark.init; lookup fracmkmk.numrspacing { pos mark zero.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark one.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark two.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark three.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark four.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark five.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark six.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark seven.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark eight.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; pos mark nine.numr - mark @NUMRNUMR; + mark @NUMRNUMR.fracmkmk.numrspacing; } fracmkmk.numrspacing; # Features diff --git a/Tests/voltLib/parser_test.py b/Tests/voltLib/parser_test.py index abc02d3b0d..d1d7b230a5 100644 --- a/Tests/voltLib/parser_test.py +++ b/Tests/voltLib/parser_test.py @@ -594,18 +594,18 @@ def test_substitution_invalid_many_to_many(self): ).statements def test_substitution_invalid_reverse_chaining_single(self): - with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"): - [lookup] = self.parse( - 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' - "ALL DIRECTION LTR REVERSAL\n" - "IN_CONTEXT\n" - "END_CONTEXT\n" - "AS_SUBSTITUTION\n" - 'SUB GLYPH "f" GLYPH "i"\n' - 'WITH GLYPH "f_i"\n' - "END_SUB\n" - "END_SUBSTITUTION" - ).statements + [lookup] = self.parse( + 'DEF_LOOKUP "invalid_substitution" PROCESS_BASE PROCESS_MARKS ' + "ALL DIRECTION LTR REVERSAL\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_SUBSTITUTION\n" + 'SUB GLYPH "f" GLYPH "i"\n' + 'WITH GLYPH "f_i"\n' + "END_SUB\n" + "END_SUBSTITUTION" + ).statements + self.assertIsInstance(lookup.sub, ast.SubstitutionLigatureDefinition) def test_substitution_invalid_mixed(self): with self.assertRaisesRegex(VoltLibError, r"Invalid substitution type"): @@ -952,7 +952,27 @@ def test_substitution_multiple_to_single(self): self.assertSubEqual(lookup.sub, [["f", "i"], ["f", "t"]], [["f_i"], ["f_t"]]) def test_substitution_reverse_chaining_single(self): - [lookup] = self.parse( + lookup = self.parse( + 'DEF_GLYPH "zero" ID 163 UNICODE 48 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "one" ID 194 UNICODE 49 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "two" ID 195 UNICODE 50 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "three" ID 196 UNICODE 51 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "four" ID 197 UNICODE 52 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "fize" ID 165 UNICODE 53 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "six" ID 209 UNICODE 54 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "seven" ID 210 UNICODE 55 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "eight" ID 211 UNICODE 56 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "nine" ID 212 UNICODE 57 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "zero.numr" ID 213 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "one.numr" ID 214 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "two.numr" ID 215 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "three.numr" ID 216 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "four.numr" ID 217 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "fize.numr" ID 218 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "six.numr" ID 219 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "seven.numr" ID 220 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "eight.numr" ID 221 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "nine.numr" ID 222 TYPE BASE END_GLYPH\n' 'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' "DIRECTION LTR REVERSAL\n" "IN_CONTEXT\n" @@ -966,7 +986,7 @@ def test_substitution_reverse_chaining_single(self): 'WITH RANGE "zero.numr" TO "nine.numr"\n' "END_SUB\n" "END_SUBSTITUTION" - ).statements + ).statements[-1] mapping = lookup.sub.mapping glyphs = [[(r.start, r.end) for r in v] for v in mapping.keys()] diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 0d8d8d289b..60cb5badcf 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -3,6 +3,7 @@ import tempfile import unittest from io import StringIO +from textwrap import dedent from fontTools.voltLib.voltToFea import VoltToFea @@ -10,6 +11,8 @@ class ToFeaTest(unittest.TestCase): + maxDiff = 10000000 + @classmethod def setup_class(cls): cls.tempdir = None @@ -30,11 +33,11 @@ def temp_path(cls): def test_def_glyph_base(self): fea = self.parse('DEF_GLYPH ".notdef" ID 0 TYPE BASE END_GLYPH') self.assertEqual( - fea, "@GDEF_base = [.notdef];\n" "table GDEF {\n" " GlyphClassDef @GDEF_base, , , ;\n" "} GDEF;\n", + fea, ) def test_def_glyph_base_2_components(self): @@ -42,46 +45,46 @@ def test_def_glyph_base_2_components(self): 'DEF_GLYPH "glyphBase" ID 320 TYPE BASE COMPONENTS 2 END_GLYPH' ) self.assertEqual( - fea, "@GDEF_base = [glyphBase];\n" "table GDEF {\n" " GlyphClassDef @GDEF_base, , , ;\n" "} GDEF;\n", + fea, ) def test_def_glyph_ligature_2_components(self): fea = self.parse('DEF_GLYPH "f_f" ID 320 TYPE LIGATURE COMPONENTS 2 END_GLYPH') self.assertEqual( - fea, "@GDEF_ligature = [f_f];\n" "table GDEF {\n" " GlyphClassDef , @GDEF_ligature, , ;\n" "} GDEF;\n", + fea, ) def test_def_glyph_mark(self): fea = self.parse('DEF_GLYPH "brevecomb" ID 320 TYPE MARK END_GLYPH') self.assertEqual( - fea, "@GDEF_mark = [brevecomb];\n" "table GDEF {\n" " GlyphClassDef , , @GDEF_mark, ;\n" "} GDEF;\n", + fea, ) def test_def_glyph_component(self): fea = self.parse('DEF_GLYPH "f.f_f" ID 320 TYPE COMPONENT END_GLYPH') self.assertEqual( - fea, "@GDEF_component = [f.f_f];\n" "table GDEF {\n" " GlyphClassDef , , , @GDEF_component;\n" "} GDEF;\n", + fea, ) def test_def_glyph_no_type(self): fea = self.parse('DEF_GLYPH "glyph20" ID 20 END_GLYPH') - self.assertEqual(fea, "") + self.assertEqual("", fea) def test_def_glyph_case_sensitive(self): fea = self.parse( @@ -89,11 +92,11 @@ def test_def_glyph_case_sensitive(self): 'DEF_GLYPH "a" ID 4 UNICODE 97 TYPE BASE END_GLYPH\n' ) self.assertEqual( - fea, "@GDEF_base = [A a];\n" "table GDEF {\n" " GlyphClassDef @GDEF_base, , , ;\n" "} GDEF;\n", + fea, ) def test_def_group_glyphs(self): @@ -105,10 +108,10 @@ def test_def_group_glyphs(self): "END_GROUP\n" ) self.assertEqual( - fea, "# Glyph classes\n" "@aaccented = [aacute abreve acircumflex adieresis ae" " agrave amacron aogonek aring atilde];", + fea, ) def test_def_group_groups(self): @@ -124,11 +127,11 @@ def test_def_group_groups(self): "END_GROUP\n" ) self.assertEqual( - fea, "# Glyph classes\n" "@Group1 = [a b c d];\n" "@Group2 = [e f g h];\n" "@TestGroup = [@Group1 @Group2];", + fea, ) def test_def_group_groups_not_yet_defined(self): @@ -150,13 +153,46 @@ def test_def_group_groups_not_yet_defined(self): "END_GROUP\n" ) self.assertEqual( - fea, "# Glyph classes\n" "@Group1 = [a b c d];\n" "@Group2 = [e f g h];\n" "@TestGroup1 = [@Group1 @Group2];\n" "@TestGroup2 = [@Group2];\n" "@TestGroup3 = [@Group2 @Group1];", + fea, + ) + + def test_def_group_groups_not_yet_defined_nested(self): + fea = self.parse( + """ + DEF_GROUP "Group1" + ENUM GLYPH "a" GLYPH "b" GLYPH "c" GLYPH "d" END_ENUM + END_GROUP + DEF_GROUP "TestGroup1" + ENUM ENUM GROUP "Group1" GROUP "Group2" END_ENUM END_ENUM + END_GROUP + DEF_GROUP "TestGroup2" + ENUM ENUM ENUM ENUM GROUP "Group2" END_ENUM END_ENUM END_ENUM END_ENUM + END_GROUP + DEF_GROUP "TestGroup3" + ENUM GROUP "Group2" GROUP "Group1" END_ENUM + END_GROUP + DEF_GROUP "Group2" + ENUM GLYPH "e" GLYPH "f" GLYPH "g" GLYPH "h" END_ENUM + END_GROUP + """ + ) + self.assertEqual( + dedent( + """\ + # Glyph classes + @Group1 = [a b c d]; + @Group2 = [e f g h]; + @TestGroup1 = [@Group1 @Group2]; + @TestGroup2 = [@Group2]; + @TestGroup3 = [@Group2 @Group1];""" + ), + fea, ) def test_def_group_glyphs_and_group(self): @@ -171,11 +207,11 @@ def test_def_group_glyphs_and_group(self): "END_GROUP" ) self.assertEqual( - fea, "# Glyph classes\n" "@aaccented = [aacute abreve acircumflex adieresis ae" " agrave amacron aogonek aring atilde];\n" "@KERN_lc_a_2ND = [a @aaccented];", + fea, ) def test_def_group_range(self): @@ -195,19 +231,19 @@ def test_def_group_range(self): "END_GROUP" ) self.assertEqual( - fea, "# Glyph classes\n" - "@KERN_lc_a_2ND = [a - atilde b c - cdotaccent];\n" + "@KERN_lc_a_2ND = [a agrave aacute acircumflex atilde b c ccaron ccedilla cdotaccent];\n" "@GDEF_base = [a agrave aacute acircumflex atilde c" " ccaron ccedilla cdotaccent];\n" "table GDEF {\n" " GlyphClassDef @GDEF_base, , , ;\n" "} GDEF;\n", + fea, ) def test_script_without_langsys(self): fea = self.parse('DEF_SCRIPT NAME "Latin" TAG "latn"\n' "END_SCRIPT") - self.assertEqual(fea, "") + self.assertEqual("", fea) def test_langsys_normal(self): fea = self.parse( @@ -218,7 +254,7 @@ def test_langsys_normal(self): "END_LANGSYS\n" "END_SCRIPT" ) - self.assertEqual(fea, "") + self.assertEqual("", fea) def test_langsys_no_script_name(self): fea = self.parse( @@ -227,7 +263,7 @@ def test_langsys_no_script_name(self): "END_LANGSYS\n" "END_SCRIPT" ) - self.assertEqual(fea, "") + self.assertEqual("", fea) def test_langsys_lang_in_separate_scripts(self): fea = self.parse( @@ -244,7 +280,7 @@ def test_langsys_lang_in_separate_scripts(self): "END_LANGSYS\n" "END_SCRIPT" ) - self.assertEqual(fea, "") + self.assertEqual("", fea) def test_langsys_no_lang_name(self): fea = self.parse( @@ -253,7 +289,46 @@ def test_langsys_no_lang_name(self): "END_LANGSYS\n" "END_SCRIPT" ) - self.assertEqual(fea, "") + self.assertEqual("", fea) + + def test_langsys_short_tag(self): + fea = self.parse( + """ + DEF_SCRIPT NAME "Latin" TAG "latn" + DEF_LANGSYS NAME "Romanian" TAG "ROM" + DEF_FEATURE NAME "Fractions" TAG "frac" + LOOKUP "test" + END_FEATURE + END_LANGSYS + END_SCRIPT + DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_SUBSTITUTION + SUB GLYPH "one" GLYPH "slash" GLYPH "two" + WITH GLYPH "one_slash_two.frac" + END_SUB + END_SUBSTITUTION + """ + ) + self.assertEqual( + dedent( + """ + # Lookups + lookup test { + sub one slash two by one_slash_two.frac; + } test; + + # Features + feature frac { + script latn; + language ROM exclude_dflt; + lookup test; + } frac; + """ + ), + fea, + ) def test_feature(self): fea = self.parse( @@ -275,7 +350,6 @@ def test_feature(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup fraclookup {\n" " sub one slash two by one_slash_two.frac;\n" @@ -287,6 +361,7 @@ def test_feature(self): " language ROM exclude_dflt;\n" " lookup fraclookup;\n" "} frac;\n", + fea, ) def test_feature_sub_lookups(self): @@ -319,7 +394,6 @@ def test_feature_sub_lookups(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup fraclookup {\n" " lookupflag RightToLeft;\n" @@ -336,6 +410,7 @@ def test_feature_sub_lookups(self): " language ROM exclude_dflt;\n" " lookup fraclookup;\n" "} frac;\n", + fea, ) def test_lookup_comment(self): @@ -355,13 +430,13 @@ def test_lookup_comment(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup smcp {\n" " # Smallcaps lookup for testing\n" " sub a by a.sc;\n" " sub b by b.sc;\n" "} smcp;\n", + fea, ) def test_substitution_single(self): @@ -382,12 +457,12 @@ def test_substitution_single(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup smcp {\n" " sub a by a.sc;\n" " sub b by b.sc;\n" "} smcp;\n", + fea, ) def test_substitution_single_in_context(self): @@ -409,15 +484,23 @@ def test_substitution_single_in_context(self): "END_SUBSTITUTION" ) self.assertEqual( + dedent( + """\ + # Glyph classes + @Denominators = [one.dnom two.dnom]; + + # Lookups + lookup fracdnom_chained { + sub one by one.dnom; + sub two by two.dnom; + } fracdnom_chained; + + lookup fracdnom { + sub [@Denominators fraction] [one two]' lookup fracdnom_chained; + } fracdnom; + """ + ), fea, - "# Glyph classes\n" - "@Denominators = [one.dnom two.dnom];\n" - "\n" - "# Lookups\n" - "lookup fracdnom {\n" - " sub [@Denominators fraction] one' by one.dnom;\n" - " sub [@Denominators fraction] two' by two.dnom;\n" - "} fracdnom;\n", ) def test_substitution_single_in_contexts(self): @@ -441,15 +524,23 @@ def test_substitution_single_in_contexts(self): "END_SUBSTITUTION" ) self.assertEqual( + dedent( + """\ + # Glyph classes + @Hebrew = [uni05D0 uni05D1]; + + # Lookups + lookup HebrewCurrency_chained { + sub dollar by dollar.Hebr; + } HebrewCurrency_chained; + + lookup HebrewCurrency { + sub dollar' lookup HebrewCurrency_chained @Hebrew one.Hebr; + sub @Hebrew one.Hebr dollar' lookup HebrewCurrency_chained; + } HebrewCurrency; + """ + ), fea, - "# Glyph classes\n" - "@Hebrew = [uni05D0 uni05D1];\n" - "\n" - "# Lookups\n" - "lookup HebrewCurrency {\n" - " sub dollar' @Hebrew one.Hebr by dollar.Hebr;\n" - " sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n" - "} HebrewCurrency;\n", ) def test_substitution_single_except_context(self): @@ -473,15 +564,23 @@ def test_substitution_single_except_context(self): "END_SUBSTITUTION" ) self.assertEqual( + dedent( + """\ + # Glyph classes + @Hebrew = [uni05D0 uni05D1]; + + # Lookups + lookup HebrewCurrency_chained { + sub dollar by dollar.Hebr; + } HebrewCurrency_chained; + + lookup HebrewCurrency { + ignore sub dollar' @Hebrew one.Hebr; + sub @Hebrew one.Hebr dollar' lookup HebrewCurrency_chained; + } HebrewCurrency; + """ + ), fea, - "# Glyph classes\n" - "@Hebrew = [uni05D0 uni05D1];\n" - "\n" - "# Lookups\n" - "lookup HebrewCurrency {\n" - " ignore sub dollar' @Hebrew one.Hebr;\n" - " sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n" - "} HebrewCurrency;\n", ) def test_substitution_skip_base(self): @@ -499,7 +598,6 @@ def test_substitution_skip_base(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -508,6 +606,7 @@ def test_substitution_skip_base(self): " lookupflag IgnoreBaseGlyphs;\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_process_base(self): @@ -525,7 +624,6 @@ def test_substitution_process_base(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -533,6 +631,7 @@ def test_substitution_process_base(self): "lookup SomeSub {\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_process_marks_all(self): @@ -550,7 +649,6 @@ def test_substitution_process_marks_all(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -558,6 +656,7 @@ def test_substitution_process_marks_all(self): "lookup SomeSub {\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_process_marks_none(self): @@ -575,7 +674,6 @@ def test_substitution_process_marks_none(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -584,6 +682,7 @@ def test_substitution_process_marks_none(self): " lookupflag IgnoreMarks;\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_skip_marks(self): @@ -601,7 +700,6 @@ def test_substitution_skip_marks(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -610,6 +708,7 @@ def test_substitution_skip_marks(self): " lookupflag IgnoreMarks;\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_mark_attachment(self): @@ -626,7 +725,6 @@ def test_substitution_mark_attachment(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [acutecmb gravecmb];\n" "\n" @@ -636,6 +734,7 @@ def test_substitution_mark_attachment(self): " @SomeMarks;\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_mark_glyph_set(self): @@ -652,7 +751,6 @@ def test_substitution_mark_glyph_set(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [acutecmb gravecmb];\n" "\n" @@ -662,6 +760,7 @@ def test_substitution_mark_glyph_set(self): " @SomeMarks;\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_process_all_marks(self): @@ -678,7 +777,6 @@ def test_substitution_process_all_marks(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [acutecmb gravecmb];\n" "\n" @@ -687,6 +785,7 @@ def test_substitution_process_all_marks(self): " lookupflag RightToLeft;\n" " sub A by A.c2sc;\n" "} SomeSub;\n", + fea, ) def test_substitution_no_reversal(self): @@ -704,11 +803,19 @@ def test_substitution_no_reversal(self): "END_SUBSTITUTION" ) self.assertEqual( + dedent( + """ + # Lookups + lookup Lookup_chained { + sub a by a.alt; + } Lookup_chained; + + lookup Lookup { + sub a' lookup Lookup_chained [a b]; + } Lookup; + """ + ), fea, - "\n# Lookups\n" - "lookup Lookup {\n" - " sub a' [a b] by a.alt;\n" - "} Lookup;\n", ) def test_substitution_reversal(self): @@ -731,7 +838,6 @@ def test_substitution_reversal(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@DFLT_Num_standardFigures = [zero one two];\n" "@DFLT_Num_numerators = [zero.numr one.numr two.numr];\n" @@ -740,6 +846,7 @@ def test_substitution_reversal(self): "lookup RevLookup {\n" " rsub @DFLT_Num_standardFigures' [a b] by @DFLT_Num_numerators;\n" "} RevLookup;\n", + fea, ) def test_substitution_single_to_multiple(self): @@ -758,12 +865,12 @@ def test_substitution_single_to_multiple(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup ccmp {\n" " sub aacute by a acutecomb;\n" " sub agrave by a gravecomb;\n" "} ccmp;\n", + fea, ) def test_substitution_multiple_to_single(self): @@ -782,16 +889,107 @@ def test_substitution_multiple_to_single(self): "END_SUBSTITUTION" ) self.assertEqual( + "\n# Lookups\n" + "lookup liga {\n" + " sub f i by f_i;\n" + " sub f t by f_t;\n" + "} liga;\n", + fea, + ) + + def test_substitution_multiple_to_single_with_groups(self): + fea = self.parse( + 'DEF_GROUP "g1" ENUM GLYPH "i" GLYPH "t" END_ENUM END_GROUP\n' + 'DEF_GROUP "g2" ENUM GLYPH "f_i" GLYPH "f_t" END_ENUM END_GROUP\n' + 'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL ' + "DIRECTION LTR\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_SUBSTITUTION\n" + 'SUB GLYPH "f" GROUP "g1"\n' + 'WITH GROUP "g2"\n' + "END_SUB\n" + "END_SUBSTITUTION" + ) + self.assertEqual( + "# Glyph classes\n" + "@g1 = [i t];\n" + "@g2 = [f_i f_t];\n" + "\n# Lookups\n" + "lookup liga {\n" + " # sub f @g1 by @g2;\n" + " sub f i by f_i;\n" + " sub f t by f_t;\n" + "} liga;\n", fea, + ) + + def test_substitution_multiple_to_single_with_enums(self): + fea = self.parse( + 'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL ' + "DIRECTION LTR\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_SUBSTITUTION\n" + 'SUB GLYPH "f" ENUM GLYPH "i" GLYPH "i.alt" END_ENUM\n' + 'WITH GLYPH "f_i"\n' + "END_SUB\n" + "END_SUBSTITUTION" + ) + self.assertEqual( "\n# Lookups\n" "lookup liga {\n" + " # sub f [i i.alt] by f_i;\n" + " sub f i by f_i;\n" + " sub f i.alt by f_i;\n" + "} liga;\n", + fea, + ) + + def test_substitution_multiple_to_single_with_enums_long_replacement(self): + fea = self.parse( + 'DEF_LOOKUP "liga" PROCESS_BASE PROCESS_MARKS ALL ' + "DIRECTION LTR\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_SUBSTITUTION\n" + 'SUB GLYPH "f" ENUM GLYPH "i" GLYPH "t" END_ENUM\n' + 'WITH ENUM GLYPH "f_i" GLYPH "f_t" GLYPH "f_l" END_ENUM\n' + "END_SUB\n" + "END_SUBSTITUTION" + ) + self.assertEqual( + "\n# Lookups\n" + "lookup liga {\n" + " # sub f [i t] by [f_i f_t f_l];\n" " sub f i by f_i;\n" " sub f t by f_t;\n" "} liga;\n", + fea, ) def test_substitution_reverse_chaining_single(self): fea = self.parse( + 'DEF_GLYPH "zero" ID 163 UNICODE 48 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "one" ID 194 UNICODE 49 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "two" ID 195 UNICODE 50 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "three" ID 196 UNICODE 51 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "four" ID 197 UNICODE 52 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "fize" ID 165 UNICODE 53 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "six" ID 209 UNICODE 54 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "seven" ID 210 UNICODE 55 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "eight" ID 211 UNICODE 56 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "nine" ID 212 UNICODE 57 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "zero.numr" ID 213 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "one.numr" ID 214 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "two.numr" ID 215 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "three.numr" ID 216 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "four.numr" ID 217 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "fize.numr" ID 218 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "six.numr" ID 219 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "seven.numr" ID 220 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "eight.numr" ID 221 TYPE BASE END_GLYPH\n' + 'DEF_GLYPH "nine.numr" ID 222 TYPE BASE END_GLYPH\n' 'DEF_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' "DIRECTION LTR REVERSAL\n" "IN_CONTEXT\n" @@ -807,11 +1005,50 @@ def test_substitution_reverse_chaining_single(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup numr {\n" - " rsub zero - nine' [fraction zero.numr - nine.numr] by zero.numr - nine.numr;\n" - "} numr;\n", + " rsub [zero one two three four fize six seven eight nine]' " + "[fraction zero.numr one.numr two.numr three.numr four.numr fize.numr six.numr seven.numr eight.numr nine.numr] by " + "[zero.numr one.numr two.numr three.numr four.numr fize.numr six.numr seven.numr eight.numr nine.numr];\n" + "} numr;\n" + "\n@GDEF_base = [zero one two three four fize six seven eight nine zero.numr one.numr two.numr three.numr four.numr fize.numr six.numr seven.numr eight.numr nine.numr];\n" + "table GDEF {\n" + " GlyphClassDef @GDEF_base, , , ;\n" + "} GDEF;\n", + fea, + ) + + def test_substitution_alternate(self): + fea = self.parse( + 'DEF_GROUP "b" ENUM GLYPH "b" END_ENUM END_GROUP\n' + 'DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL ' + "DIRECTION LTR\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_SUBSTITUTION\n" + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.alt1"\n' + "END_SUB\n" + 'SUB GLYPH "a"\n' + 'WITH GLYPH "a.alt2"\n' + "END_SUB\n" + 'SUB GROUP "b"\n' + 'WITH GROUP "b"\n' + "END_SUB\n" + 'SUB GLYPH "b"\n' + 'WITH GLYPH "b.alt"\n' + "END_SUB\n" + "END_SUBSTITUTION" + ) + self.assertEqual( + "# Glyph classes\n" + "@b = [b];\n" + "\n# Lookups\n" + "lookup test {\n" + " sub a from [a.alt1 a.alt2];\n" + " sub b from [b b.alt];\n" + "} test;\n", + fea, ) # GPOS @@ -841,19 +1078,19 @@ def test_position_attach(self): "AT POS DX 215 DY 450 END_POS END_ANCHOR\n" ) self.assertEqual( - fea, "\n# Mark classes\n" - "markClass acutecomb @top;\n" - "markClass gravecomb @top;\n" + "markClass acutecomb @top.anchor_top;\n" + "markClass gravecomb @top.anchor_top;\n" "\n" "# Lookups\n" "lookup anchor_top {\n" " lookupflag RightToLeft;\n" " pos base a\n" - " mark @top;\n" + " mark @top.anchor_top;\n" " pos base e\n" - " mark @top;\n" + " mark @top.anchor_top;\n" "} anchor_top;\n", + fea, ) def test_position_attach_mkmk(self): @@ -875,21 +1112,21 @@ def test_position_attach_mkmk(self): "AT POS DX 210 DY 450 END_POS END_ANCHOR\n" ) self.assertEqual( - fea, "\n# Mark classes\n" - "markClass acutecomb @top;\n" + "markClass acutecomb @top.anchor_top;\n" "\n" "# Lookups\n" "lookup anchor_top {\n" " lookupflag RightToLeft;\n" " pos mark gravecomb\n" - " mark @top;\n" + " mark @top.anchor_top;\n" "} anchor_top;\n" "\n" "@GDEF_mark = [brevecomb gravecomb];\n" "table GDEF {\n" " GlyphClassDef , , @GDEF_mark, ;\n" "} GDEF;\n", + fea, ) def test_position_attach_in_context(self): @@ -911,22 +1148,99 @@ def test_position_attach_in_context(self): "AT POS DX 210 DY 450 END_POS END_ANCHOR\n" ) self.assertEqual( - fea, "\n# Mark classes\n" - "markClass acutecomb @top;\n" - "markClass gravecomb @top;\n" + "markClass acutecomb @top.test;\n" + "markClass gravecomb @top.test;\n" "\n" "# Lookups\n" - "lookup test_target {\n" + "lookup test_chained {\n" " pos base a\n" - " mark @top;\n" - "} test_target;\n" + " mark @top.test;\n" + "} test_chained;\n" "\n" "lookup test {\n" " lookupflag RightToLeft;\n" " ignore pos a [acutecomb gravecomb]';\n" - " pos [acutecomb gravecomb]' lookup test_target;\n" + " pos [acutecomb gravecomb]' lookup test_chained;\n" "} test;\n", + fea, + ) + + def test_position_attach_overlapping_classes(self): + fea = self.parse( + 'DEF_GROUP "above_marks"\n' + 'ENUM GLYPH "acutecomb" GLYPH "gravecomb" END_ENUM\n' + "END_GROUP\n" + 'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR\n' + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_POSITION\n" + 'ATTACH GLYPH "a" GLYPH "e"\n' + 'TO GLYPH "gravecomb" AT ANCHOR "top2" GROUP "above_marks" AT ANCHOR "top"\n' + "END_ATTACH\n" + "END_POSITION\n" + 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "MARK_top2" ON 121 GLYPH gravecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 AT POS DX 210 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 AT POS DX 215 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top2" ON 31 GLYPH a COMPONENT 1 AT POS DX 210 DY 550 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top2" ON 35 GLYPH e COMPONENT 1 AT POS DX 215 DY 550 END_POS END_ANCHOR\n' + ) + self.assertEqual( + "# Glyph classes\n" + "@above_marks = [acutecomb gravecomb];\n" + "\n# Mark classes\n" + "markClass acutecomb @top.anchor_top;\n" + "markClass gravecomb @top2.anchor_top;\n" + "\n" + "# Lookups\n" + "lookup anchor_top {\n" + " pos base a\n" + " mark @top2.anchor_top\n" + " mark @top.anchor_top;\n" + " pos base e\n" + " mark @top2.anchor_top\n" + " mark @top.anchor_top;\n" + "} anchor_top;\n", + fea, + ) + + def test_position_attach_overlapping_classes_dropped(self): + fea = self.parse( + 'DEF_GROUP "above_marks"\n' + 'ENUM GLYPH "gravecomb" END_ENUM\n' + "END_GROUP\n" + 'DEF_LOOKUP "anchor_top" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR\n' + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_POSITION\n" + 'ATTACH GLYPH "a" GLYPH "e"\n' + 'TO GLYPH "gravecomb" AT ANCHOR "top2" GROUP "above_marks" AT ANCHOR "top"\n' + "END_ATTACH\n" + "END_POSITION\n" + 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "MARK_top" ON 121 GLYPH gravecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "MARK_top2" ON 121 GLYPH gravecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top" ON 31 GLYPH a COMPONENT 1 AT POS DX 210 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top" ON 35 GLYPH e COMPONENT 1 AT POS DX 215 DY 450 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top2" ON 31 GLYPH a COMPONENT 1 AT POS DX 210 DY 550 END_POS END_ANCHOR\n' + 'DEF_ANCHOR "top2" ON 35 GLYPH e COMPONENT 1 AT POS DX 215 DY 550 END_POS END_ANCHOR\n' + ) + self.assertEqual( + "# Glyph classes\n" + "@above_marks = [gravecomb];\n" + "\n# Mark classes\n" + "markClass gravecomb @top2.anchor_top;\n" + "\n" + "# Lookups\n" + "lookup anchor_top {\n" + " pos base a\n" + " mark @top2.anchor_top;\n" + " pos base e\n" + " mark @top2.anchor_top;\n" + "} anchor_top;\n", + fea, ) def test_position_attach_cursive(self): @@ -946,7 +1260,6 @@ def test_position_attach_cursive(self): 'DEF_ANCHOR "entry" ON 3 GLYPH c COMPONENT 1 AT POS END_POS END_ANCHOR\n' ) self.assertEqual( - fea, "\n# Lookups\n" "lookup SomeLookup {\n" " lookupflag RightToLeft;\n" @@ -954,6 +1267,7 @@ def test_position_attach_cursive(self): " pos cursive c ;\n" " pos cursive b ;\n" "} SomeLookup;\n", + fea, ) def test_position_adjust_pair(self): @@ -972,13 +1286,13 @@ def test_position_adjust_pair(self): "END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup kern1 {\n" " lookupflag RightToLeft;\n" " enum pos A V -30;\n" " enum pos V A -25;\n" "} kern1;\n", + fea, ) def test_position_adjust_pair_in_context(self): @@ -995,16 +1309,16 @@ def test_position_adjust_pair_in_context(self): "END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" - "lookup kern1_target {\n" + "lookup kern1_chained {\n" " enum pos V A -25;\n" - "} kern1_target;\n" + "} kern1_chained;\n" "\n" "lookup kern1 {\n" " ignore pos A V' A';\n" - " pos V' lookup kern1_target A' lookup kern1_target;\n" + " pos V' lookup kern1_chained A' lookup kern1_chained;\n" "} kern1;\n", + fea, ) def test_position_adjust_single(self): @@ -1021,12 +1335,12 @@ def test_position_adjust_single(self): "END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup TestLookup {\n" " pos glyph1 <123 0 0 0>;\n" " pos glyph2 <456 0 0 0>;\n" "} TestLookup;\n", + fea, ) def test_position_adjust_single_in_context(self): @@ -1045,17 +1359,17 @@ def test_position_adjust_single_in_context(self): "END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" - "lookup TestLookup_target {\n" + "lookup TestLookup_chained {\n" " pos glyph1 <123 0 0 0>;\n" " pos glyph2 <456 0 0 0>;\n" - "} TestLookup_target;\n" + "} TestLookup_chained;\n" "\n" "lookup TestLookup {\n" " ignore pos leftGlyph [glyph1 glyph2]' rightGlyph;\n" - " pos [glyph1 glyph2]' lookup TestLookup_target;\n" + " pos [glyph1 glyph2]' lookup TestLookup_chained;\n" "} TestLookup;\n", + fea, ) def test_def_anchor(self): @@ -1075,15 +1389,50 @@ def test_def_anchor(self): "COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR" ) self.assertEqual( - fea, "\n# Mark classes\n" - "markClass acutecomb @top;\n" + "markClass acutecomb @top.TestLookup;\n" "\n" "# Lookups\n" "lookup TestLookup {\n" " pos base a\n" - " mark @top;\n" + " mark @top.TestLookup;\n" "} TestLookup;\n", + fea, + ) + + def test_def_anchor_case_insensitive(self): + fea = self.parse( + """ + DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_POSITION + ATTACH ENUM GLYPH "a" GLYPH "f_i" END_ENUM + TO GLYPH "acutecomb" AT ANCHOR "top" + END_ATTACH + END_POSITION + DEF_ANCHOR "top" ON 1 GLYPH a COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "TOP" ON 2 GLYPH f_i COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "Top" ON 2 GLYPH f_i COMPONENT 2 AT POS DX 350 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "MARK_Top" ON 3 GLYPH acutecomb COMPONENT 1 AT POS DX 0 DY 450 END_POS END_ANCHOR + """ + ) + self.assertEqual( + dedent( + """ + # Mark classes + markClass acutecomb @top.TestLookup; + + # Lookups + lookup TestLookup { + pos base a + mark @top.TestLookup; + pos base f_i + mark @top.TestLookup; + } TestLookup; + """ + ), + fea, ) def test_def_anchor_multi_component(self): @@ -1106,39 +1455,236 @@ def test_def_anchor_multi_component(self): "COMPONENT 1 AT POS END_POS END_ANCHOR" ) self.assertEqual( + "\n# Mark classes\n" + "markClass acutecomb @top.TestLookup;\n" + "\n" + "# Lookups\n" + "lookup TestLookup {\n" + " pos ligature f_f\n" + " mark @top.TestLookup\n" + " ligComponent\n" + " mark @top.TestLookup;\n" + "} TestLookup;\n" + "\n" + "@GDEF_ligature = [f_f];\n" + "table GDEF {\n" + " GlyphClassDef , @GDEF_ligature, , ;\n" + "} GDEF;\n", fea, + ) + + def test_def_anchor_ligature_missing_component(self): + fea = self.parse( + 'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL ' + "DIRECTION LTR\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_POSITION\n" + 'ATTACH GLYPH "f_f"\n' + 'TO GLYPH "acutecomb" AT ANCHOR "top"\n' + "END_ATTACH\n" + "END_POSITION\n" + 'DEF_GLYPH "f_f" ID 120 TYPE LIGATURE COMPONENTS 2 END_GLYPH\n' + 'DEF_ANCHOR "top" ON 120 GLYPH f_f ' + "COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR\n" + 'DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb ' + "COMPONENT 1 AT POS END_POS END_ANCHOR" + ) + self.assertEqual( "\n# Mark classes\n" - "markClass acutecomb @top;\n" + "markClass acutecomb @top.TestLookup;\n" "\n" "# Lookups\n" "lookup TestLookup {\n" " pos ligature f_f\n" - " mark @top\n" + " mark @top.TestLookup\n" " ligComponent\n" - " mark @top;\n" + " ;\n" "} TestLookup;\n" "\n" "@GDEF_ligature = [f_f];\n" "table GDEF {\n" " GlyphClassDef , @GDEF_ligature, , ;\n" "} GDEF;\n", + fea, + ) + + def test_def_anchor_ligature_in_base_lookup(self): + fea = self.parse( + """ + DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_POSITION + ATTACH GLYPH "f_f" GLYPH "f" + TO GLYPH "acutecomb" AT ANCHOR "top" + END_ATTACH + END_POSITION + DEF_GLYPH "f" ID 120 TYPE BASE END_GLYPH + DEF_GLYPH "f_f" ID 121 TYPE LIGATURE COMPONENTS 2 END_GLYPH + DEF_ANCHOR "top" ON 120 GLYPH f COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "top" ON 120 GLYPH f_f COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "top" ON 120 GLYPH f_f COMPONENT 2 AT POS DX 450 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 AT POS END_POS END_ANCHOR + """ + ) + self.assertEqual( + dedent( + """ + # Mark classes + markClass acutecomb @top.TestLookup; + + # Lookups + lookup TestLookup { + pos base f_f + mark @top.TestLookup; + pos base f + mark @top.TestLookup; + } TestLookup; + + @GDEF_base = [f]; + @GDEF_ligature = [f_f]; + table GDEF { + GlyphClassDef @GDEF_base, @GDEF_ligature, , ; + } GDEF; + """ + ), + fea, + ) + + def test_def_anchor_mark_in_base_lookup(self): + fea = self.parse( + """ + DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_POSITION + ATTACH GLYPH "acutecomb" GLYPH "f" + TO GLYPH "acutecomb" AT ANCHOR "top" + END_ATTACH + END_POSITION + DEF_GLYPH "f" ID 120 TYPE BASE END_GLYPH + DEF_GLYPH "acutecomb" ID 121 TYPE MARK END_GLYPH + DEF_ANCHOR "top" ON 120 GLYPH f COMPONENT 1 AT POS DX 250 DY 450 END_POS END_ANCHOR + DEF_ANCHOR "top" ON 120 GLYPH acutecomb COMPONENT 1 AT POS END_POS END_ANCHOR + DEF_ANCHOR "MARK_top" ON 120 GLYPH acutecomb COMPONENT 1 AT POS END_POS END_ANCHOR + """ + ) + self.assertEqual( + dedent( + """ + # Mark classes + markClass acutecomb @top.TestLookup; + + # Lookups + lookup TestLookup { + pos base acutecomb + mark @top.TestLookup; + pos base f + mark @top.TestLookup; + } TestLookup; + + @GDEF_base = [f]; + @GDEF_mark = [acutecomb]; + table GDEF { + GlyphClassDef @GDEF_base, , @GDEF_mark, ; + } GDEF; + """ + ), + fea, ) def test_anchor_adjust_device(self): fea = self.parse( 'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph ' "COMPONENT 1 AT POS DX 0 DY 456 ADJUST_BY 12 AT 34 " - "ADJUST_BY 56 AT 78 END_POS END_ANCHOR" + "ADJUST_BY 56 AT 78 END_POS END_ANCHOR\n" + 'DEF_ANCHOR "top" ON 121 GLYPH baseglyph ' + "COMPONENT 1 AT POS DX 0 DY 0" + "END_POS END_ANCHOR\n" + 'DEF_LOOKUP "test" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR\n' + "AS_POSITION\n" + 'ATTACH GLYPH "baseglyph"\n' + 'TO GLYPH "diacglyph" AT ANCHOR "top"\n' + "END_ATTACH\n" + "END_POSITION" ) self.assertEqual( - fea, "\n# Mark classes\n" - "#markClass diacglyph " - " > @top;", + "markClass diacglyph " + " > @top.test;\n" + "\n# Lookups\n" + "lookup test {\n" + " pos base baseglyph\n" + " mark @top.test;\n" + "} test;\n", + fea, + ) + + def test_nested_enum(self): + fea = self.parse( + """ + DEF_GLYPH "a" ID 1 END_GLYPH + DEF_GLYPH "b" ID 2 END_GLYPH + DEF_GLYPH "c" ID 3 END_GLYPH + DEF_GLYPH "d" ID 4 END_GLYPH + DEF_GLYPH "e" ID 5 END_GLYPH + DEF_GLYPH "f" ID 6 END_GLYPH + DEF_GLYPH "g" ID 7 END_GLYPH + DEF_GLYPH "h" ID 8 END_GLYPH + DEF_GLYPH "i" ID 9 END_GLYPH + DEF_GLYPH "j" ID 10 END_GLYPH + DEF_GLYPH "k" ID 11 END_GLYPH + DEF_GLYPH "l" ID 12 END_GLYPH + DEF_GLYPH "m" ID 13 END_GLYPH + DEF_GLYPH "n" ID 14 END_GLYPH + DEF_GLYPH "o" ID 15 END_GLYPH + DEF_GLYPH "p" ID 16 END_GLYPH + DEF_GLYPH "q" ID 17 END_GLYPH + DEF_LOOKUP "lookup" PROCESS_BASE SKIP_MARKS DIRECTION RTL + IN_CONTEXT + RIGHT GLYPH "space" + END_CONTEXT + AS_POSITION + ADJUST_SINGLE + ENUM GLYPH "a" GLYPH "b" END_ENUM BY POS ADV -10 DX -10 END_POS + ENUM GLYPH "c" GLYPH "d" END_ENUM BY POS ADV -20 DX -20 END_POS + ENUM RANGE "e" TO "f" GLYPH "g" RANGE "h" TO "k" GLYPH "l" END_ENUM BY POS ADV -30 DX -40 END_POS + RANGE "m" TO "q" BY POS ADV -50 DX -60 END_POS + END_ADJUST + END_POSITION + """ + ) + self.assertEqual( + dedent( + """ + # Lookups + lookup lookup_chained { + pos [a b] <-10 0 -10 0>; + pos [c d] <-20 0 -20 0>; + pos [e f g h i j k l] <-40 0 -30 0>; + pos [m n o p q] <-60 0 -50 0>; + } lookup_chained; + + lookup lookup { + lookupflag RightToLeft IgnoreMarks; + pos [a b c d e f g h i j k l m n o p q]' lookup lookup_chained space; + } lookup; + """ + ), + fea, ) def test_use_extension(self): fea = self.parse( + 'DEF_LOOKUP "liga1" PROCESS_BASE PROCESS_MARKS ALL ' + "DIRECTION LTR\n" + "IN_CONTEXT\n" + "END_CONTEXT\n" + "AS_SUBSTITUTION\n" + 'SUB GLYPH "f" GLYPH "i" WITH GLYPH "f_i"\n' + "END_SUB\n" + "END_SUBSTITUTION\n" 'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL ' "DIRECTION LTR\n" "IN_CONTEXT\n" @@ -1154,18 +1700,22 @@ def test_use_extension(self): "COMPILER_USEEXTENSIONLOOKUPS\n" ) self.assertEqual( - fea, "\n# Lookups\n" + "lookup liga1 useExtension {\n" + " sub f i by f_i;\n" + "} liga1;\n" + "\n" "lookup kern1 useExtension {\n" " enum pos A V -30;\n" " enum pos V A -25;\n" "} kern1;\n", + fea, ) def test_unsupported_compiler_flags(self): with self.assertLogs(level="WARNING") as logs: fea = self.parse("CMAP_FORMAT 0 3 4") - self.assertEqual(fea, "") + self.assertEqual("", fea) self.assertEqual( logs.output, [ @@ -1183,7 +1733,6 @@ def test_sanitize_lookup_name(self): "AS_POSITION ADJUST_PAIR END_ADJUST END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup Test_Lookup {\n" " \n" @@ -1192,6 +1741,7 @@ def test_sanitize_lookup_name(self): "lookup Test_Lookup_ {\n" " \n" "} Test_Lookup_;\n", + fea, ) def test_sanitize_group_name(self): @@ -1204,10 +1754,103 @@ def test_sanitize_group_name(self): "END_GROUP\n" ) self.assertEqual( - fea, "# Glyph classes\n" "@aaccented_glyphs = [aacute abreve];\n" "@aaccented_glyphs_ = [aacute abreve];", + fea, + ) + + def test_group_nested_enum(self): + fea = self.parse( + 'DEF_GROUP "foo"\n' + 'ENUM ENUM GLYPH "foo" GLYPH "foo.1" GLYPH "foo.2" END_ENUM END_ENUM\n' + "END_GROUP" + ) + self.assertEqual("# Glyph classes\n@foo = [foo foo.1 foo.2];", fea) + + def test_aalt_feature(self): + with self.assertLogs(level="WARNING") as logs: + fea = self.parse( + """ + DEF_SCRIPT NAME "Latin" TAG "latn" + DEF_LANGSYS NAME "English" TAG "ENG " + DEF_FEATURE NAME "Access All Alternates" TAG "aalt" + LOOKUP "test1" + END_FEATURE + END_LANGSYS + END_SCRIPT + DEF_SCRIPT NAME "Default" TAG "DFLT" + DEF_LANGSYS NAME "English" TAG "ENG " + DEF_FEATURE NAME "Access All Alternates" TAG "aalt" + LOOKUP "test2" + END_FEATURE + END_LANGSYS + DEF_LANGSYS NAME "Default" TAG "dflt" + DEF_FEATURE NAME "Access All Alternates" TAG "aalt" + LOOKUP "test3" + END_FEATURE + END_LANGSYS + END_SCRIPT + DEF_LOOKUP "test1" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_SUBSTITUTION + SUB GLYPH "a" + WITH GLYPH "a.alt" + END_SUB + END_SUBSTITUTION + DEF_LOOKUP "test2" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_SUBSTITUTION + SUB GLYPH "b" + WITH GLYPH "b.alt" + END_SUB + END_SUBSTITUTION + DEF_LOOKUP "test3" PROCESS_BASE PROCESS_MARKS ALL DIRECTION LTR + IN_CONTEXT + END_CONTEXT + AS_SUBSTITUTION + SUB GLYPH "c" + WITH GLYPH "c.alt" + END_SUB + END_SUBSTITUTION + """ + ) + self.assertEqual( + dedent( + """ + # Lookups + lookup test1 { + sub a by a.alt; + } test1; + + lookup test2 { + sub b by b.alt; + } test2; + + lookup test3 { + sub c by c.alt; + } test3; + + # Features + feature aalt { + lookup test3; + } aalt; + """ + ), + fea, + ) + self.assertEqual( + logs.output, + [ + "WARNING:fontTools.voltLib.voltToFea:FEA syntax does not allow script " + "statements in 'aalt' feature, so only lookups from the first script will be " + "included.", + "WARNING:fontTools.voltLib.voltToFea:FEA syntax does not allow language " + "statements in 'aalt' feature, so only lookups from the first language will be " + "included.", + ], ) def test_cli_vtp(self): diff --git a/mypy.ini b/mypy.ini index 0a04cfb23c..6fd134d679 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.7 +python_version = 3.9 files = Lib/fontTools/misc/plistlib follow_imports = silent ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 5a8e741b09..f478815bb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,2 @@ [tool.black] -target-version = ["py37"] +target-version = ["py39"] diff --git a/requirements.txt b/requirements.txt index 8a834edd2a..bcef14cd21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,8 @@ brotli==1.1.0; platform_python_implementation != "PyPy" brotlicffi==1.1.0.0; platform_python_implementation == "PyPy" unicodedata2==15.1.0; python_version <= '3.11' -scipy==1.10.0; platform_python_implementation != "PyPy" and python_version <= '3.8' # pyup: ignore -scipy==1.14.1; platform_python_implementation != "PyPy" and python_version >= '3.9' +scipy==1.13.1; platform_python_implementation != "PyPy" and python_version <= '3.9' # pyup: ignore +scipy==1.14.1; platform_python_implementation != "PyPy" and python_version >= '3.10' munkres==1.1.4; platform_python_implementation == "PyPy" zopfli==0.2.3.post1 fs==2.4.16 diff --git a/setup.cfg b/setup.cfg index 148357a191..c280c349f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.57.0 +current_version = 4.58.0 commit = True tag = False tag_name = {new_version} diff --git a/setup.py b/setup.py index 64a49258b2..1d9f1ac9f9 100755 --- a/setup.py +++ b/setup.py @@ -158,11 +158,9 @@ def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): "Environment :: Other Environment", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -495,7 +493,7 @@ def build_extensions(self): setup_params = dict( name="fonttools", - version="4.57.0", + version="4.58.0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com", @@ -503,8 +501,9 @@ def build_extensions(self): maintainer_email="behdad@behdad.org", url="http://github.com/fonttools/fonttools", license="MIT", + license_files=["LICENSE", "LICENSE.external"], platforms=["Any"], - python_requires=">=3.8", + python_requires=">=3.9", long_description=long_description, long_description_content_type="text/x-rst", package_dir={"": "Lib"}, diff --git a/tox.ini b/tox.ini index 256fc873a5..b9364c0841 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.0 -envlist = lint, py3{8,9,10,11,12,13}-cov, htmlcov +envlist = lint, py3{9,10,11,12,13}-cov, htmlcov skip_missing_interpreters=true [testenv] @@ -58,38 +58,19 @@ commands = [testenv:package_readme] description = check that the long description is valid (need for PyPi) -deps = twine >= 1.12.1 - pip >= 18.0.0 -skip_install = true -extras = -commands = pip wheel -w {envtmpdir}/build --no-deps . - twine check {envtmpdir}/build/* - -[testenv:bdist] deps = - setuptools - wheel + twine >= 1.12.1 + uv skip_install = true -install_command = - # make sure we use the latest setuptools and wheel - pip install --upgrade {opts} {packages} -whitelist_externals = - rm +extras = commands = - # clean up build/ and dist/ folders - python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)' - python setup.py clean --all - # build sdist - python setup.py sdist --dist-dir {toxinidir}/dist - # build wheel from sdist - pip wheel -v --no-deps --no-index --wheel-dir {toxinidir}/dist --find-links {toxinidir}/dist fonttools + uv build --quiet --wheel --out-dir {envtmpdir}/build + twine check {envtmpdir}/build/* -[testenv:pypi] +[testenv:docs] +description = Build the documentation. deps = - {[testenv:bdist]deps} - twine -skip_install = true -passenv = TWINE_USERNAME TWINE_PASSWORD + -r requirements.txt + -r Doc/docs-requirements.txt commands = - {[testenv:bdist]commands} - twine upload dist/*.whl dist/*.zip + sphinx-build -W -j auto Doc/source Doc/build