diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2cf71c2752..ba0a193ee9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,13 +35,14 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.11", "3.12"] + # Oldest supported, 'Stable' supported, Newest supported + 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 - python-version: 3.11 + python-version: 3.12 - platform: windows-latest - python-version: 3.11 + python-version: 3.12 steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -60,7 +61,7 @@ jobs: coverage combine coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: coverage.xml flags: unittests diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 712d1bb340..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 @@ -47,14 +42,18 @@ jobs: shell: bash strategy: fail-fast: false + # macos-13 runners are still x86_64, macos-14 (latest) are arm64; we want to build + # the x86_64 wheel on/for x86_64 macs matrix: - os: [macos-latest, windows-latest, ubuntu-latest] + os: [macos-13, windows-latest, ubuntu-latest] arch: [auto64] include: - os: macos-latest arch: universal2 - os: windows-latest arch: auto32 + - os: ubuntu-24.04-arm + arch: aarch64 steps: - uses: actions/checkout@v4 with: @@ -76,40 +75,12 @@ jobs: name: wheels-${{ matrix.os }}-${{ matrix.arch }} path: wheelhouse/*.whl - build_arch_wheels: - name: py${{ matrix.python }} on ${{ matrix.arch }} - runs-on: ubuntu-latest - strategy: - matrix: - # aarch64 uses qemu so it's slow, build each py version in parallel jobs - python: [38, 39, 310, 311, 312] - arch: [aarch64] - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - uses: docker/setup-qemu-action@v3.0.0 - with: - platforms: all - - name: Install dependencies - run: pip install cibuildwheel - - name: Build Wheels - run: python -m cibuildwheel --output-dir wheelhouse . - env: - CIBW_BUILD: cp${{ matrix.python }}-* - CIBW_ARCHS: ${{ matrix.arch }} - - uses: actions/upload-artifact@v4 - with: - name: wheels-py${{ matrix.python }}-linux-${{ matrix.arch }} - path: wheelhouse/*.whl - deploy: name: Upload to PyPI on tagged commit runs-on: ubuntu-latest needs: - build_pure - build_wheels - - build_arch_wheels # only run if the commit is tagged... if: startsWith(github.ref, 'refs/tags/') steps: @@ -118,7 +89,7 @@ jobs: # so that all artifacts are downloaded in the same directory specified by 'path' merge-multiple: true path: dist - - uses: pypa/gh-action-pypi-publish@v1.8.14 + - uses: pypa/gh-action-pypi-publish@v1.12.4 with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea116c410a..f2022b279c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ If you are unfamiliar with that, check out [opensource.guide](https://opensource We use Github's Issue Tracker to report, discuss and track bugs, map out future improvements, set priorities, and self-assign issues. If you find a bug, have an idea for a new feature, then please [create a new issue](https://github.com/fonttools/fonttools/issues) and we'll be happy to work with you on it! -If you have a question or want to discuss usage from an end-user perspective, there is a mailing list at [groups.google.com/d/forum/fonttools](https://groups.google.com/d/forum/fonttools) mailing list. +If you have a question or want to discuss usage from an end-user perspective, check out the [Discussions](https://github.com/fonttools/fonttools/discussions). If you would like to speak to someone directly, you can also email the project lead, Behdad Esfahbod, privately at 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/docs-requirements.txt b/Doc/docs-requirements.txt index 9397ab3058..a7a6ee106f 100644 --- a/Doc/docs-requirements.txt +++ b/Doc/docs-requirements.txt @@ -1,4 +1,4 @@ -sphinx==7.3.7 -sphinx_rtd_theme==2.0.0 -reportlab==4.2.0 -freetype-py==2.4.0 +sphinx==8.1.3 +sphinx_rtd_theme==3.0.2 +reportlab==4.2.5 +freetype-py==2.5.1 diff --git a/Doc/source/afmLib.rst b/Doc/source/afmLib.rst index ab9f356784..7e233698c6 100644 --- a/Doc/source/afmLib.rst +++ b/Doc/source/afmLib.rst @@ -1,8 +1,15 @@ -########################################### -afmLib: Read/write Adobe Font Metrics files -########################################### +############################################### +afmLib: Read and write Adobe Font Metrics files +############################################### +.. rubric:: Overview: + :heading-level: 2 + .. automodule:: fontTools.afmLib + :members: + :undoc-members: -.. autoclass:: fontTools.afmLib.AFM - :members: + + .. rubric:: Module members: + :heading-level: 2 + diff --git a/Doc/source/agl.rst b/Doc/source/agl.rst index 6e89857f42..aff58a3623 100644 --- a/Doc/source/agl.rst +++ b/Doc/source/agl.rst @@ -2,5 +2,15 @@ agl: Interface to the Adobe Glyph List ###################################### +.. rubric:: Overview: + :heading-level: 2 + .. automodule:: fontTools.agl - :members: toUnicode, UV2AGL, AGL2UV + :members: + :undoc-members: + :member-order: bysource + + + .. rubric:: Module members: + :heading-level: 2 + diff --git a/Doc/source/cffLib/CFF2ToCFF.rst b/Doc/source/cffLib/CFF2ToCFF.rst new file mode 100644 index 0000000000..28719cafe6 --- /dev/null +++ b/Doc/source/cffLib/CFF2ToCFF.rst @@ -0,0 +1,10 @@ +############################## +CFF2ToCFF: convert CFF2 to CFF +############################## + +.. automodule:: fontTools.cffLib.CFF2ToCFF + :inherited-members: + :members: + :undoc-members: + + diff --git a/Doc/source/cffLib/CFFToCFF2.rst b/Doc/source/cffLib/CFFToCFF2.rst new file mode 100644 index 0000000000..3fb0f20b39 --- /dev/null +++ b/Doc/source/cffLib/CFFToCFF2.rst @@ -0,0 +1,8 @@ +############################## +CFFToCFF2: convert CFF to CFF2 +############################## + +.. automodule:: fontTools.cffLib.CFFToCFF2 + :inherited-members: + :members: + :undoc-members: diff --git a/Doc/source/cffLib/index.rst b/Doc/source/cffLib/index.rst index 4a40f92f41..19761cdff0 100644 --- a/Doc/source/cffLib/index.rst +++ b/Doc/source/cffLib/index.rst @@ -1,13 +1,38 @@ -################################## -cffLib: read/write Adobe CFF fonts -################################## +###################################### +cffLib: Read and write Adobe CFF fonts +###################################### + +.. rubric:: Overview: + :heading-level: 3 + .. automodule:: fontTools.cffLib + :inherited-members: + :members: + :undoc-members: + :member-order: bysource -This package also contains two modules for manipulating CFF format glyphs: + .. rubric:: Submodules: + :heading-level: 3 -.. toctree:: - :maxdepth: 1 + cffLib contains submodules for converting between CCF and CFF2. + Each can be used within other Python scripts or run as a + subcommand of the ``fonttools`` command-line tool: - specializer - width + .. toctree:: + :maxdepth: 1 + + CFFToCFF2 + CFF2ToCFF + + It also contains submodules for manipulating CFF-formatted glyphs: + + .. toctree:: + :maxdepth: 1 + + specializer + width + + .. rubric:: Module members: + :heading-level: 3 + diff --git a/Doc/source/cffLib/specializer.rst b/Doc/source/cffLib/specializer.rst index 016a896216..b14f56ca37 100644 --- a/Doc/source/cffLib/specializer.rst +++ b/Doc/source/cffLib/specializer.rst @@ -2,7 +2,15 @@ specializer: T2CharString operator specializer and generalizer ############################################################## +.. rubric:: Overview: + :heading-level: 3 + .. automodule:: fontTools.cffLib.specializer :inherited-members: :members: :undoc-members: + + + .. rubric:: Module members: + :heading-level: 3 + diff --git a/Doc/source/colorLib/index.rst b/Doc/source/colorLib/index.rst index 5a9bf8a160..f9447f6225 100644 --- a/Doc/source/colorLib/index.rst +++ b/Doc/source/colorLib/index.rst @@ -3,3 +3,4 @@ colorLib.builder: Build COLR/CPAL tables from scratch ##################################################### .. automodule:: fontTools.colorLib.builder + :no-inherited-members: diff --git a/Doc/source/conf.py b/Doc/source/conf.py index 982af8032c..4879ab3012 100644 --- a/Doc/source/conf.py +++ b/Doc/source/conf.py @@ -31,16 +31,25 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "sphinx.ext.napoleon", "sphinx.ext.autodoc", "sphinx.ext.viewcode", - "sphinx.ext.napoleon", "sphinx.ext.coverage", "sphinx.ext.autosectionlabel", ] -autodoc_mock_imports = ["gtk", "reportlab"] +autodoc_mock_imports = ["gtk", "reportlab", "Quartz"] -autodoc_default_options = {"members": True, "inherited-members": True} +autodoc_default_options = { + "members": True, + "inherited-members": False, + "show-inheritance": True, + "member-order": "bysource", +} + +# This option allows show-inheritace to work but not clutter up the output +# with the (surplus) full inheritance stack. +autodoc_inherit_docstrings = False # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -78,13 +87,16 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = [] +# The programming language to be used by default in syntax highlighting. +highlight_language = "python" + # The name of the Pygments (syntax highlighting) style to use. # pygments_style = "sphinx" (the default sphinx docs style on RTD) pygments_style = "default" diff --git a/Doc/source/config.rst b/Doc/source/config.rst index 6be575d5ef..0b98d66831 100644 --- a/Doc/source/config.rst +++ b/Doc/source/config.rst @@ -1,8 +1,7 @@ ########################### -config: configure fontTools +config: Configure fontTools ########################### .. automodule:: fontTools.config - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/cu2qu/index.rst b/Doc/source/cu2qu/index.rst index 41730e54bb..1affa21a72 100644 --- a/Doc/source/cu2qu/index.rst +++ b/Doc/source/cu2qu/index.rst @@ -1,6 +1,6 @@ -########################################## -cu2qu: Cubic to quadratic curve conversion -########################################## +######################################## +cu2qu: Convert cubic curves to quadratic +######################################## Routines for converting cubic curves to quadratic splines, suitable for use in OpenType to TrueType outline conversion. diff --git a/Doc/source/designspaceLib/index.rst b/Doc/source/designspaceLib/index.rst index 6bf2975420..13b77b6409 100644 --- a/Doc/source/designspaceLib/index.rst +++ b/Doc/source/designspaceLib/index.rst @@ -4,7 +4,7 @@ designspaceLib: Read, write, and edit designspace files Implements support for reading and manipulating ``designspace`` files. Allows the users to define axes, rules, sources, variable fonts and instances, -and their STAT information. +and their ``STAT`` information. .. toctree:: :maxdepth: 1 diff --git a/Doc/source/designspaceLib/xml.rst b/Doc/source/designspaceLib/xml.rst index 7b59dbb17a..f5645b8ca4 100644 --- a/Doc/source/designspaceLib/xml.rst +++ b/Doc/source/designspaceLib/xml.rst @@ -297,7 +297,7 @@ Example of all axis elements together ```` element -................... +.................... - Defines the output location of an axis mapping. - Child element of ```` @@ -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/developer.rst b/Doc/source/developer.rst index e480706afd..abaea79af2 100644 --- a/Doc/source/developer.rst +++ b/Doc/source/developer.rst @@ -1,4 +1,5 @@ :orphan: + .. _developerinfo: .. image:: ../../Icons/FontToolsIconGreenCircle.png :width: 200px diff --git a/Doc/source/encodings/index.rst b/Doc/source/encodings/index.rst index 32d13c70e8..97f73d38e9 100644 --- a/Doc/source/encodings/index.rst +++ b/Doc/source/encodings/index.rst @@ -1,21 +1,65 @@ -################################################## -encodings: Support for OpenType-specific encodings -################################################## +############################################################ +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 61ac31f3bc..a0236ab431 100644 --- a/Doc/source/feaLib/index.rst +++ b/Doc/source/feaLib/index.rst @@ -1,11 +1,37 @@ -######################################### -feaLib: Read/write OpenType feature files -######################################### +############################################# +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 51dfc3c117..e191a33e80 100644 --- a/Doc/source/index.rst +++ b/Doc/source/index.rst @@ -6,7 +6,7 @@ ---fontTools Documentation--- -======= +============================= About ----- @@ -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 `_: @@ -87,12 +87,10 @@ License fontTools is licensed under the `MIT license `_. Refer to the full text of the license for details. -Table of Contents ------------------ .. toctree:: - :maxdepth: 2 - :caption: Library + :maxdepth: 1 + :hidden: afmLib agl @@ -108,6 +106,7 @@ Table of Contents mtiLib otlLib/index pens/index + qu2cu/index subset/index svgLib/index t1Lib @@ -115,7 +114,6 @@ Table of Contents ttLib/index ttx ufoLib/index - unicode unicodedata/index varLib/index voltLib/index diff --git a/Doc/source/merge.rst b/Doc/source/merge.rst index 3114615571..819f6ea7e8 100644 --- a/Doc/source/merge.rst +++ b/Doc/source/merge.rst @@ -2,9 +2,12 @@ merge: Merge multiple fonts into one #################################### -``fontTools.merge`` provides both a library and a command line interface +.. rubric:: Overview: + :heading-level: 2 + +:mod:`fontTools.merge` provides both a library and a command line interface (``fonttools merge``) for merging multiple fonts together. .. autoclass:: fontTools.merge.Merger - :inherited-members: :members: + :undoc-members: diff --git a/Doc/source/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/mtiLib.rst b/Doc/source/mtiLib.rst index 1bf74e189f..d90bd9b72f 100644 --- a/Doc/source/mtiLib.rst +++ b/Doc/source/mtiLib.rst @@ -2,13 +2,27 @@ mtiLib: Read Monotype FontDame source files ########################################### -FontTools provides support for reading the OpenType layout tables produced by +.. rubric:: Overview + :heading-level: 2 + +FontTools provides support for reading the OpenType Layout tables produced by Monotype's FontDame and Font Chef font editors. These tables are written in a -simple textual format. The ``mtiLib`` library parses these text files and creates +simple textual format. The :py:mod:`mtiLib` library parses these text files and creates table objects representing their contents. -Additionally, ``fonttools mtiLib`` will convert a text file to TTX XML. +Additionally, running the ``fonttools mtiLib`` command will convert a text file to TTX XML. + + +.. rubric:: Module members: + :heading-level: 2 + + +.. autofunction:: fontTools.mtiLib.build +.. autofunction:: fontTools.mtiLib.main + .. automodule:: fontTools.mtiLib - :members: build, main + :members: + :undoc-members: + :exclude-members: build, main 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 272db73a4b..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. @@ -107,3 +113,11 @@ STAT Table Builder .. currentmodule:: fontTools.otlLib.builder .. autofunction:: buildStatTable + +------------------ +MATH Table Builder +------------------ + +.. currentmodule:: fontTools.otlLib.builder + +.. autofunction:: buildMathTable 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 0f76b7238f..bad859de07 100644 --- a/Doc/source/pens/index.rst +++ b/Doc/source/pens/index.rst @@ -1,24 +1,82 @@ -#### -pens -#### +########################################### +pens: Inspect and manipulate glyph outlines +########################################### + + +.. rubric:: Overview: + :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. + +Some pens trace through the outlines and generate graphical output, +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 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 general Pen +Protocol is documented on the :doc:`basePen ` page: .. toctree:: - :maxdepth: 1 + :maxdepth: 2 - areaPen 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. + + + +.. toctree:: + :maxdepth: 2 + + areaPen boundsPen - cocoaPen cu2quPen filterPen - freetypePen momentsPen perimeterPen pointInsidePen - pointPen - qtPen recordingPen - reportLabPen reverseContourPen roundingPen statisticsPen @@ -27,5 +85,15 @@ pens 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/qu2cu/cli.rst b/Doc/source/qu2cu/cli.rst new file mode 100644 index 0000000000..25c5fb78b7 --- /dev/null +++ b/Doc/source/qu2cu/cli.rst @@ -0,0 +1,13 @@ +################################################### +cli: Quadratic-to-cubic converter command-line tool +################################################### + +.. rubric:: Overview: + :heading-level: 2 + +.. automodule:: fontTools.qu2cu.cli + :members: + :undoc-members: + + .. rubric:: Module members: + :heading-level: 2 diff --git a/Doc/source/qu2cu/index.rst b/Doc/source/qu2cu/index.rst new file mode 100644 index 0000000000..4a64b743fd --- /dev/null +++ b/Doc/source/qu2cu/index.rst @@ -0,0 +1,38 @@ +######################################## +qu2cu: Convert quadratic curves to cubic +######################################## + +.. rubric:: Overview + :heading-level: 2 + + +Routines for converting quadratic curves to cubic splines, suitable for use +in TrueType to CFF-flavored OpenType outline conversion. + +The basic curve conversion routines are implemented in the +:mod:`fontTools.qu2cu.qu2cu` module. + +.. note:: The redundancy in the module name is a workaround made + made necessary by :mod:`fontTools.qu2cu`'s usage of + `Cython `_. Providing Cython support + for the module enables faster execution on systems where + Cython is available. However, the module remains fully + available on systems without Cython, too. + +.. automodule:: fontTools.qu2cu.qu2cu + :members: + :undoc-members: + + qu2cu also includes a submodule that implements the + ``fonttools qu2cu`` command for converting a UFO format font with + quadratic curves into one with cubic curves: + + .. toctree:: + :maxdepth: 1 + + cli + + + .. rubric:: Package contents + :heading-level: 2 + 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 f32e0d42c9..30067801d0 100644 --- a/Doc/source/subset/index.rst +++ b/Doc/source/subset/index.rst @@ -1,6 +1,8 @@ -###### -subset -###### +######################################################## +subset: Generate subsets of fonts or optimize file sizes +######################################################## + +.. currentmodule:: fontTools.subset .. toctree:: :maxdepth: 1 @@ -8,6 +10,5 @@ subset cff .. automodule:: fontTools.subset - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/svgLib/index.rst b/Doc/source/svgLib/index.rst index f86ff0ad47..6f1715b1dc 100644 --- a/Doc/source/svgLib/index.rst +++ b/Doc/source/svgLib/index.rst @@ -1,6 +1,6 @@ -###### -svgLib -###### +############################################ +svgLib: Read and write SVG-in-OpenType fonts +############################################ .. toctree:: :maxdepth: 1 diff --git a/Doc/source/t1Lib.rst b/Doc/source/t1Lib.rst index 4436086fd1..b9a8a8ea6f 100644 --- a/Doc/source/t1Lib.rst +++ b/Doc/source/t1Lib.rst @@ -1,8 +1,18 @@ -##### -t1Lib -##### +############################################# +t1Lib: Read and write PostScript Type 1 fonts +############################################# + +.. rubric:: Overview: + :heading-level: 2 + +Note also that :mod:`t1Lib` supports some :doc:`optional ` +external libraries. .. automodule:: fontTools.t1Lib - :inherited-members: :members: :undoc-members: + + + .. rubric:: Module members: + :heading-level: 2 + diff --git a/Doc/source/tfmLib.rst b/Doc/source/tfmLib.rst index daa4125c2f..7e3a612652 100644 --- a/Doc/source/tfmLib.rst +++ b/Doc/source/tfmLib.rst @@ -2,7 +2,18 @@ tfmLib: Read TeX Font Metrics files ########################################### +.. rubric:: Overview + :heading-level: 2 + + .. automodule:: fontTools.tfmLib + :members: TFM + :undoc-members: + :exclude-members: TFMException + + + .. rubric:: Module contents: + :heading-level: 2 -.. autoclass:: fontTools.tfmLib.TFM - :members: + +.. autoexception:: fontTools.tfmLib.TFMException diff --git a/Doc/source/ttLib/index.rst b/Doc/source/ttLib/index.rst index 7798238da9..0f5e3946ae 100644 --- a/Doc/source/ttLib/index.rst +++ b/Doc/source/ttLib/index.rst @@ -1,8 +1,14 @@ -############################################# -ttLib: Read/write OpenType and TrueType fonts -############################################# +################################################# +ttLib: Read and write OpenType and TrueType fonts +################################################# -Most users of the fontTools library will be using it to generate or manipulate +.. contents:: On this page: + :local: + +.. rubric:: Overview: + :heading-level: 2 + +Most users of the :mod:`fontTools` library will be using it to generate or manipulate OpenType and TrueType fonts. (FontTools initially only supported TrueType fonts, gaining OpenType support in version 2.0, and so uses the ``tt`` prefix to refer to both kinds of font. Because of this we will refer to both as "TrueType fonts" @@ -10,14 +16,46 @@ unless we need to make a distinction.) The main entry point for such operations is the :py:mod:`fontTools.ttLib.ttFont` module, but other modules also provide useful functionality for handling OpenType -fonts. +fonts: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 ttFont + +:mod:`.ttLib` supports fonts with TrueType-flavored glyphs (i.e., with +a ``glyf`` table), with PostScript-flavored glyphs (i.e., ``CFF`` or +``CFF2`` tables), and with all of the glyph formats used for color fonts +(``CBDT``, ``COLR``, ``sbix``, and ``SVG``). Static and variable fonts +are both supported. + + +Command-line utilities +---------------------- + +:mod:`.ttLib` includes two modules that provide command-line operations: + +.. toctree:: + :maxdepth: 1 + + removeOverlaps + scaleUpem + + +Supporting modules +------------------ + +It also contains helper modules that enable lower-level +functionality. In most cases, users will not need to access these +modules directly: + +.. toctree:: + :maxdepth: 1 + ttCollection + ttGlyphSet macUtils + reorderGlyphs sfnt standardGlyphOrder tables diff --git a/Doc/source/ttLib/macUtils.rst b/Doc/source/ttLib/macUtils.rst index 356a911b21..81413301f5 100644 --- a/Doc/source/ttLib/macUtils.rst +++ b/Doc/source/ttLib/macUtils.rst @@ -1,8 +1,7 @@ -######## -macUtils -######## +########################################## +macUtils: Access fonts in Mac file formats +########################################## .. automodule:: fontTools.ttLib.macUtils - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/removeOverlaps.rst b/Doc/source/ttLib/removeOverlaps.rst new file mode 100644 index 0000000000..83e99922b6 --- /dev/null +++ b/Doc/source/ttLib/removeOverlaps.rst @@ -0,0 +1,13 @@ +############################################################## +removeOverlaps: Tools to remove overlapping contours in glyphs +############################################################## + +.. rubric:: Overview + :heading-level: 2 + +The :mod:`fontTools.ttLib.removeOverlaps` module is a helper for +:mod:`fontTools.ttLib`. + +.. automodule:: fontTools.ttLib.removeOverlaps + :members: + :undoc-members: diff --git a/Doc/source/ttLib/reorderGlyphs.rst b/Doc/source/ttLib/reorderGlyphs.rst new file mode 100644 index 0000000000..95635588d2 --- /dev/null +++ b/Doc/source/ttLib/reorderGlyphs.rst @@ -0,0 +1,13 @@ +######################################################## +reorderGlyphs: Functions to reorder the glyphs in a font +######################################################## + +.. rubric:: Overview + :heading-level: 2 + +The :mod:`fontTools.ttLib.reorderGlyphs` module is a helper for +:mod:`fontTools.ttLib`. + +.. automodule:: fontTools.ttLib.reorderGlyphs + :members: + :undoc-members: diff --git a/Doc/source/ttLib/scaleUpem.rst b/Doc/source/ttLib/scaleUpem.rst new file mode 100644 index 0000000000..4ac16b2d28 --- /dev/null +++ b/Doc/source/ttLib/scaleUpem.rst @@ -0,0 +1,13 @@ +########################################################### +scaleUpem: Tools to change the units-per-Em value in a font +########################################################### + +.. rubric:: Overview + :heading-level: 2 + +The :mod:`fontTools.ttLib.scaleUpem` module is a helper for +:mod:`fontTools.ttLib`. + +.. automodule:: fontTools.ttLib.scaleUpem + :members: + :undoc-members: diff --git a/Doc/source/ttLib/sfnt.rst b/Doc/source/ttLib/sfnt.rst index 29566ab74f..a1fa433c30 100644 --- a/Doc/source/ttLib/sfnt.rst +++ b/Doc/source/ttLib/sfnt.rst @@ -1,8 +1,7 @@ -#### -sfnt -#### +######################################### +sfnt: Read and write the SFNT file format +######################################### .. automodule:: fontTools.ttLib.sfnt - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/standardGlyphOrder.rst b/Doc/source/ttLib/standardGlyphOrder.rst index ca2557bf30..6cb7b4e0dd 100644 --- a/Doc/source/ttLib/standardGlyphOrder.rst +++ b/Doc/source/ttLib/standardGlyphOrder.rst @@ -1,9 +1,8 @@ -################## -standardGlyphOrder -################## +########################################################### +standardGlyphOrder: Interface with the Standard Glyph Order +########################################################### .. automodule:: fontTools.ttLib.standardGlyphOrder - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables.rst b/Doc/source/ttLib/tables.rst index 4f8df8d443..ac508e22bf 100644 --- a/Doc/source/ttLib/tables.rst +++ b/Doc/source/ttLib/tables.rst @@ -1,13 +1,131 @@ -############################### -TrueType/OpenType Table Modules -############################### +####################################### +tables: Access TrueType/OpenType tables +####################################### + +.. contents:: On this page + :local: + + +.. rubric:: Overview + :heading-level: 2 This folder is a subpackage of :py:mod:`fontTools.ttLib`. Each module here is a -specialized TT/OT table converter: they can convert raw data -to Python objects and vice versa. Usually you don't need to -use the modules directly: they are imported and used -automatically when needed by :py:mod:`fontTools.ttLib`. The tables currently -supported are: +specialized TrueType/OpenType table converter: they can convert raw data +to Python objects and vice versa. Usually you do not need to +use these modules directly: they are imported and used +automatically when needed by :py:mod:`fontTools.ttLib`. + +In addition to the tables defined in the official TrueType/OpenType +specification documents, several specialty tables are supported that +are used by specific vendors (including the Graphite shaping engine +and Apple's AAT). Note that fontTools supports the tables that provide +the core functionality of AAT, but does not guarantee everything. + +Similarly, fontTools supports some third-party tables used by external +applications (such as FontForge and Microsoft's VTT), but may not +support every private table employed by those applications. + + +Accessing tables +---------------- + +The Python modules representing the tables have pretty strange names: this is due to the +fact that we need to map TT/OT table tags (which are case sensitive) +to filenames (which on macOS and Windows are not case sensitive) as well +as to Python identifiers. The latter means that each table identifier +can only contain ``[A-Za-z0-9_]`` and cannot start with a number. + +The convention adopted is that capital letters in a table tag are +transformed into the letter followed by an underscore (e.g., ``A_``), while lowercase +letters and numbers are preceded by an underscore (e.g., ``_a``). + +:py:mod:`fontTools.ttLib` provides functions to expand a tag into the format used here:: + + >>> from fontTools import ttLib + >>> ttLib.tagToIdentifier("FOO ") + 'F_O_O_' + >>> ttLib.tagToIdentifier("cvt ") + '_c_v_t' + >>> ttLib.tagToIdentifier("OS/2") + 'O_S_2f_2' + >>> ttLib.tagToIdentifier("glyf") + '_g_l_y_f' + >>> + +And vice versa:: + + >>> ttLib.identifierToTag("F_O_O_") + 'FOO ' + >>> ttLib.identifierToTag("_c_v_t") + 'cvt ' + >>> ttLib.identifierToTag("O_S_2f_2") + 'OS/2' + >>> ttLib.identifierToTag("_g_l_y_f") + 'glyf' + >>> + +Eg. the 'glyf' table converter lives in a Python file called:: + + _g_l_y_f.py + +The converter itself is a class, named ``table_`` + expandedtag. Eg:: + + + class table__g_l_y_f: + etc. + +Note that if you _do_ need to use such modules or classes manually, +there are two convenient API functions that let you find them by tag:: + + >>> ttLib.getTableModule('glyf') + + >>> ttLib.getTableClass('glyf') + + >> + + +Helper modules +-------------- + +In addition to the core table-conversion implementations, a set of +helper and utility modules is also found in this package. +You should not normally need to access these modules directly, +but consulting them might be valuable if you need to add fontTools +support for a new table type. + +In that case, a good place to start is with the documentation for +the base table classes: + +.. toctree:: + :maxdepth: 1 + + tables/table_api + +The modules that provide lower-level helper functionality +include implementations of common OpenType data structures, support +for OpenType font variations, and various classes needed for +tables containing bitmap data or for tables used by the Graphite engine: + +.. toctree:: + :maxdepth: 1 + + tables/OpenType_related + tables/TupleVariation + tables/Bitmap_related + tables/grUtils + +A module is also included for assembling and disassembling +TrueType bytecode: + +.. toctree:: + :maxdepth: 1 + + tables/ttProgram + + + +Tables currently supported +-------------------------- .. toctree:: :maxdepth: 1 @@ -84,106 +202,7 @@ supported are: tables/VTT_related tables/V_V_A_R_ -The Python modules representing the tables have pretty strange names: this is due to the -fact that we need to map TT table tags (which are case sensitive) -to filenames (which on Mac and Win aren't case sensitive) as well -as to Python identifiers. The latter means it can only contain -``[A-Za-z0-9_]`` and cannot start with a number. - -:py:mod:`fontTools.ttLib` provides functions to expand a tag into the format used here:: - - >>> from fontTools import ttLib - >>> ttLib.tagToIdentifier("FOO ") - 'F_O_O_' - >>> ttLib.tagToIdentifier("cvt ") - '_c_v_t' - >>> ttLib.tagToIdentifier("OS/2") - 'O_S_2f_2' - >>> ttLib.tagToIdentifier("glyf") - '_g_l_y_f' - >>> - -And vice versa:: - - >>> ttLib.identifierToTag("F_O_O_") - 'FOO ' - >>> ttLib.identifierToTag("_c_v_t") - 'cvt ' - >>> ttLib.identifierToTag("O_S_2f_2") - 'OS/2' - >>> ttLib.identifierToTag("_g_l_y_f") - 'glyf' - >>> - -Eg. the 'glyf' table converter lives in a Python file called:: - - _g_l_y_f.py - -The converter itself is a class, named ``table_`` + expandedtag. Eg:: - - - class table__g_l_y_f: - etc. - -Note that if you _do_ need to use such modules or classes manually, -there are two convenient API functions that let you find them by tag:: - - >>> ttLib.getTableModule('glyf') - - >>> ttLib.getTableClass('glyf') - - >> - -ttProgram: TrueType bytecode assembler/disassembler ---------------------------------------------------- - -.. automodule:: fontTools.ttLib.tables.ttProgram - :inherited-members: - :members: - :undoc-members: - -Contributing your own table convertors --------------------------------------- - -To add support for a new font table that fontTools does not currently implement, -you must subclass from :py:mod:`fontTools.ttLib.tables.DefaultTable.DefaultTable`. -It provides some default behavior, as well as a constructor method (``__init__``) -that you don't need to override. - -Your converter should minimally provide two methods:: - - - class table_F_O_O_(DefaultTable.DefaultTable): # converter for table 'FOO ' - - def decompile(self, data, ttFont): - # 'data' is the raw table data. Unpack it into a - # Python data structure. - # 'ttFont' is a ttLib.TTfile instance, enabling you to - # refer to other tables. Do ***not*** keep a reference to - # it: it will cause a circular reference (ttFont saves - # a reference to us), and that means we'll be leaking - # memory. If you need to use it in other methods, just - # pass it around as a method argument. - - def compile(self, ttFont): - # Return the raw data, as converted from the Python - # data structure. - # Again, 'ttFont' is there so you can access other tables. - # Same warning applies. - - -If you want to support TTX import/export as well, you need to provide two -additional methods:: - - - def toXML(self, writer, ttFont): - # XXX - - def fromXML(self, (name, attrs, content), ttFont): - # XXX - .. automodule:: fontTools.ttLib.tables - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/B_A_S_E_.rst b/Doc/source/ttLib/tables/B_A_S_E_.rst index bc0c132a6d..3cd3b5163c 100644 --- a/Doc/source/ttLib/tables/B_A_S_E_.rst +++ b/Doc/source/ttLib/tables/B_A_S_E_.rst @@ -1,8 +1,9 @@ -``BASE``: Baseline Table +``BASE``: Baseline table ------------------------ +The ``BASE`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.B_A_S_E_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/Bitmap_related.rst b/Doc/source/ttLib/tables/Bitmap_related.rst new file mode 100644 index 0000000000..01bf0d0094 --- /dev/null +++ b/Doc/source/ttLib/tables/Bitmap_related.rst @@ -0,0 +1,36 @@ +########################## +Bitmap-data helper modules +########################## + +.. contents:: On this page: + :local: + +.. rubric:: Overview: + :heading-level: 2 + +The modules documented on this page are helpers for +:mod:`fontTools.ttLib` that implement lower-level support for various +table converters that need to interact with bitmapped data. The +:mod:`.BitmapGlyphMetrics` module is used for the ``EBDT``/``EBLC`` and +``CBDT``/``CBLC`` tables, and :mod:`.sbixGlyph` and :mod:`.sbixStrike` +are used for the ``sbix`` table. + +fontTools.ttLib.tables.BitmapGlyphMetrics +----------------------------------------- +.. automodule:: fontTools.ttLib.tables.BitmapGlyphMetrics + :members: + :undoc-members: + + +fontTools.ttLib.tables.sbixGlyph +-------------------------------- +.. automodule:: fontTools.ttLib.tables.sbixGlyph + :members: + :undoc-members: + + +fontTools.ttLib.tables.sbixStrike +--------------------------------- +.. automodule:: fontTools.ttLib.tables.sbixStrike + :members: + :undoc-members: diff --git a/Doc/source/ttLib/tables/C_B_D_T_.rst b/Doc/source/ttLib/tables/C_B_D_T_.rst index 602ffb6f1b..3a06fa1324 100644 --- a/Doc/source/ttLib/tables/C_B_D_T_.rst +++ b/Doc/source/ttLib/tables/C_B_D_T_.rst @@ -1,8 +1,13 @@ -``CBDT``: Color Bitmap Data Table +``CBDT``: Color Bitmap Data table --------------------------------- +The ``CBDT`` table is an OpenType table. + +This ``CBDT`` table converter module depends on the +:mod:`.BitmapGlyphMetrics` module, which it shares with the ``EBDT``, +``EBLC``, and ``CBLC`` converters. + .. automodule:: fontTools.ttLib.tables.C_B_D_T_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/C_B_L_C_.rst b/Doc/source/ttLib/tables/C_B_L_C_.rst index 00c5ac40bc..aec847e691 100644 --- a/Doc/source/ttLib/tables/C_B_L_C_.rst +++ b/Doc/source/ttLib/tables/C_B_L_C_.rst @@ -1,8 +1,13 @@ -``CBLC``: Color Bitmap Location Table +``CBLC``: Color Bitmap Location table ------------------------------------- +The ``CBLC`` table is an OpenType table. + +This ``CBLC`` table converter module depends on the +:mod:`.BitmapGlyphMetrics` module, which it shares with the ``EBDT``, +``EBLC``, and ``CBDT`` converters. + .. automodule:: fontTools.ttLib.tables.C_B_L_C_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/C_F_F_.rst b/Doc/source/ttLib/tables/C_F_F_.rst index 92c2e5bb99..9a11b782e4 100644 --- a/Doc/source/ttLib/tables/C_F_F_.rst +++ b/Doc/source/ttLib/tables/C_F_F_.rst @@ -1,8 +1,9 @@ -``CFF``: Compact Font Format Table +``CFF``: Compact Font Format table ---------------------------------- +The ``CFF`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.C_F_F_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/C_F_F__2.rst b/Doc/source/ttLib/tables/C_F_F__2.rst index 946c246ae7..98025cd6e3 100644 --- a/Doc/source/ttLib/tables/C_F_F__2.rst +++ b/Doc/source/ttLib/tables/C_F_F__2.rst @@ -1,8 +1,9 @@ -``CFF2``: Compact Font Format (CFF) Version 2 ---------------------------------------------- +``CFF2``: Compact Font Format (CFF) Version 2 table +--------------------------------------------------- + +The ``CFF2`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables.C_F_F__2 - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/C_O_L_R_.rst b/Doc/source/ttLib/tables/C_O_L_R_.rst index 11c30d53fb..2b38f4a6f5 100644 --- a/Doc/source/ttLib/tables/C_O_L_R_.rst +++ b/Doc/source/ttLib/tables/C_O_L_R_.rst @@ -1,7 +1,8 @@ -``COLR``: Color Table +``COLR``: Color table --------------------- +The ``COLR`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.C_O_L_R_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/C_P_A_L_.rst b/Doc/source/ttLib/tables/C_P_A_L_.rst index 4ade11a77d..abde6b6966 100644 --- a/Doc/source/ttLib/tables/C_P_A_L_.rst +++ b/Doc/source/ttLib/tables/C_P_A_L_.rst @@ -1,8 +1,9 @@ -``CPAL``: Color Palette Table +``CPAL``: Color Palette table ----------------------------- +The ``CPAL`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.C_P_A_L_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/D_S_I_G_.rst b/Doc/source/ttLib/tables/D_S_I_G_.rst index a3256bb0da..d36c54076f 100644 --- a/Doc/source/ttLib/tables/D_S_I_G_.rst +++ b/Doc/source/ttLib/tables/D_S_I_G_.rst @@ -1,8 +1,9 @@ -``DSIG``: Digital Signature Table +``DSIG``: Digital Signature table --------------------------------- +The ``DSIG`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.D_S_I_G_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/E_B_D_T_.rst b/Doc/source/ttLib/tables/E_B_D_T_.rst index 8cd6e8d4dc..bc1bd5bdb0 100644 --- a/Doc/source/ttLib/tables/E_B_D_T_.rst +++ b/Doc/source/ttLib/tables/E_B_D_T_.rst @@ -1,18 +1,13 @@ -``EBDT``: Embedded Bitmap Data Table +``EBDT``: Embedded Bitmap Data table ------------------------------------ -.. automodule:: fontTools.ttLib.tables.E_B_D_T_ - :inherited-members: - :members: - :undoc-members: +The ``EBDT`` table is an OpenType table. +This ``EBDT`` table converter module depends on the +:mod:`.BitmapGlyphMetrics` module, which it shares with the ``EBLC``, +``CBDT``, and ``CBLC`` converters. -BitmapGlyphMetrics -^^^^^^^^^^^^^^^^^^ - -.. automodule:: fontTools.ttLib.tables.BitmapGlyphMetrics - :noindex: - :inherited-members: +.. automodule:: fontTools.ttLib.tables.E_B_D_T_ :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/E_B_L_C_.rst b/Doc/source/ttLib/tables/E_B_L_C_.rst index aa8a74014a..e9529b07ce 100644 --- a/Doc/source/ttLib/tables/E_B_L_C_.rst +++ b/Doc/source/ttLib/tables/E_B_L_C_.rst @@ -1,18 +1,12 @@ -``EBLC``: Embedded Bitmap Location Table +``EBLC``: Embedded Bitmap Location table ---------------------------------------- -.. automodule:: fontTools.ttLib.tables.E_B_L_C_ - :inherited-members: - :members: - :undoc-members: +The ``EBLC`` table is an OpenType table. +This ``EBLC`` table converter module depends on the +:mod:`.BitmapGlyphMetrics` module, which it shares with the ``EBDT``, +``CBDT``, and ``CBLC`` converters. -BitmapGlyphMetrics -^^^^^^^^^^^^^^^^^^ - -.. automodule:: fontTools.ttLib.tables.BitmapGlyphMetrics - :noindex: - :inherited-members: +.. automodule:: fontTools.ttLib.tables.E_B_L_C_ :members: :undoc-members: - diff --git a/Doc/source/ttLib/tables/F_F_T_M_.rst b/Doc/source/ttLib/tables/F_F_T_M_.rst index 901ac50ebd..518bf81711 100644 --- a/Doc/source/ttLib/tables/F_F_T_M_.rst +++ b/Doc/source/ttLib/tables/F_F_T_M_.rst @@ -1,8 +1,9 @@ -``FFTM``: FontForge Time Stamp Table +``FFTM``: FontForge Time Stamp table ------------------------------------ +The ``FFTM`` table is used by the FontForge font editor. + .. automodule:: fontTools.ttLib.tables.F_F_T_M_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/F__e_a_t.rst b/Doc/source/ttLib/tables/F__e_a_t.rst index 7c53026c1e..2d0ff96218 100644 --- a/Doc/source/ttLib/tables/F__e_a_t.rst +++ b/Doc/source/ttLib/tables/F__e_a_t.rst @@ -1,8 +1,9 @@ -``Feat``: Graphite Feature Table +``Feat``: Graphite Feature table -------------------------------- +The ``Feat`` table is a Graphite table. + .. automodule:: fontTools.ttLib.tables.F__e_a_t - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G_D_E_F_.rst b/Doc/source/ttLib/tables/G_D_E_F_.rst index b0e95be531..8f1200c863 100644 --- a/Doc/source/ttLib/tables/G_D_E_F_.rst +++ b/Doc/source/ttLib/tables/G_D_E_F_.rst @@ -1,8 +1,9 @@ -``GDEF``: Glyph Definition Table +``GDEF``: Glyph Definition table -------------------------------- +The ``GDEF`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.G_D_E_F_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G_M_A_P_.rst b/Doc/source/ttLib/tables/G_M_A_P_.rst index 4d1ec5b53f..0aee3cb58f 100644 --- a/Doc/source/ttLib/tables/G_M_A_P_.rst +++ b/Doc/source/ttLib/tables/G_M_A_P_.rst @@ -1,8 +1,9 @@ -``GMAP``: SING Glyphlet Summary Table +``GMAP``: SING Glyphlet Summary table ------------------------------------- +The ``GMAP`` table is an Adobe Glyphlets table. + .. automodule:: fontTools.ttLib.tables.G_M_A_P_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G_P_K_G_.rst b/Doc/source/ttLib/tables/G_P_K_G_.rst index 1cf66c859f..c37c8123ce 100644 --- a/Doc/source/ttLib/tables/G_P_K_G_.rst +++ b/Doc/source/ttLib/tables/G_P_K_G_.rst @@ -1,8 +1,9 @@ -``GPKG``: SING Glyphlet Wrapper Table +``GPKG``: SING Glyphlet Wrapper table ------------------------------------- +The ``GPKG`` table is an Adobe Glyphlets table. + .. automodule:: fontTools.ttLib.tables.G_P_K_G_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G_P_O_S_.rst b/Doc/source/ttLib/tables/G_P_O_S_.rst index a36c5b3217..b3001cadf7 100644 --- a/Doc/source/ttLib/tables/G_P_O_S_.rst +++ b/Doc/source/ttLib/tables/G_P_O_S_.rst @@ -1,8 +1,9 @@ -``GPOS``: Glyph Positioning Table +``GPOS``: Glyph Positioning table --------------------------------- +The ``GPOS`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.G_P_O_S_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G_S_U_B_.rst b/Doc/source/ttLib/tables/G_S_U_B_.rst index f9ac6c7b7c..fbe09f76fc 100644 --- a/Doc/source/ttLib/tables/G_S_U_B_.rst +++ b/Doc/source/ttLib/tables/G_S_U_B_.rst @@ -1,8 +1,9 @@ -``GSUB``: Glyph Substitution Table +``GSUB``: Glyph Substitution table ---------------------------------- +The ``GSUB`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.G_S_U_B_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G__l_a_t.rst b/Doc/source/ttLib/tables/G__l_a_t.rst index dc3d04d4c7..bf85e90943 100644 --- a/Doc/source/ttLib/tables/G__l_a_t.rst +++ b/Doc/source/ttLib/tables/G__l_a_t.rst @@ -1,8 +1,9 @@ -``Glat``: Graphite Glyph Attributes Table +``Glat``: Graphite Glyph Attributes table ----------------------------------------- +The ``Glat`` table is a Graphite table. + .. automodule:: fontTools.ttLib.tables.G__l_a_t - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/G__l_o_c.rst b/Doc/source/ttLib/tables/G__l_o_c.rst index cb43e1005b..293390006f 100644 --- a/Doc/source/ttLib/tables/G__l_o_c.rst +++ b/Doc/source/ttLib/tables/G__l_o_c.rst @@ -1,8 +1,9 @@ ``Gloc``: Graphite index to glyph attributes table -------------------------------------------------- +The ``Gloc`` table is a Graphite table. + .. automodule:: fontTools.ttLib.tables.G__l_o_c - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/H_V_A_R_.rst b/Doc/source/ttLib/tables/H_V_A_R_.rst index 14fb2734f1..e5cd824c80 100644 --- a/Doc/source/ttLib/tables/H_V_A_R_.rst +++ b/Doc/source/ttLib/tables/H_V_A_R_.rst @@ -1,7 +1,8 @@ -``HVAR``:Horizontal Metrics Variations Table --------------------------------------------- +``HVAR``: Horizontal Metrics Variations table +--------------------------------------------- + +The ``HVAR`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables.H_V_A_R_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/J_S_T_F_.rst b/Doc/source/ttLib/tables/J_S_T_F_.rst index 912a0befb0..b4c35db70f 100644 --- a/Doc/source/ttLib/tables/J_S_T_F_.rst +++ b/Doc/source/ttLib/tables/J_S_T_F_.rst @@ -1,7 +1,8 @@ -``JSTF``: Justification Table +``JSTF``: Justification table ----------------------------- +The ``JSTF`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.J_S_T_F_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/L_T_S_H_.rst b/Doc/source/ttLib/tables/L_T_S_H_.rst index 1f4c32f1a7..be506ffa47 100644 --- a/Doc/source/ttLib/tables/L_T_S_H_.rst +++ b/Doc/source/ttLib/tables/L_T_S_H_.rst @@ -1,8 +1,9 @@ -``LTSH``: Linear Threshold --------------------------- +``LTSH``: Linear Threshold table +-------------------------------- + +The ``LTSH`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables.L_T_S_H_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/M_A_T_H_.rst b/Doc/source/ttLib/tables/M_A_T_H_.rst index 1aa2ea2e30..f5bb5dfb1a 100644 --- a/Doc/source/ttLib/tables/M_A_T_H_.rst +++ b/Doc/source/ttLib/tables/M_A_T_H_.rst @@ -1,7 +1,8 @@ -``MATH``: Mathematical Typesetting Table +``MATH``: Mathematical Typesetting table ---------------------------------------- +The ``MATH`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.M_A_T_H_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/M_E_T_A_.rst b/Doc/source/ttLib/tables/M_E_T_A_.rst index f2840eae03..b49121cb56 100644 --- a/Doc/source/ttLib/tables/M_E_T_A_.rst +++ b/Doc/source/ttLib/tables/M_E_T_A_.rst @@ -1,7 +1,8 @@ -``META``: SING Glyphlet Metadata Table +``META``: SING Glyphlet Metadata table -------------------------------------- +The ``META`` table is an Adobe Glyphlets table. + .. automodule:: fontTools.ttLib.tables.M_E_T_A_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/M_V_A_R_.rst b/Doc/source/ttLib/tables/M_V_A_R_.rst index 2f5ce6739a..59b0e74e32 100644 --- a/Doc/source/ttLib/tables/M_V_A_R_.rst +++ b/Doc/source/ttLib/tables/M_V_A_R_.rst @@ -1,7 +1,8 @@ -``MVAR``: Metrics Variations Table +``MVAR``: Metrics Variations table ---------------------------------- +The ``MVAR`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.M_V_A_R_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/O_S_2f_2.rst b/Doc/source/ttLib/tables/O_S_2f_2.rst index 703502bbc0..c6655e4ed1 100644 --- a/Doc/source/ttLib/tables/O_S_2f_2.rst +++ b/Doc/source/ttLib/tables/O_S_2f_2.rst @@ -1,7 +1,8 @@ -``OS/2``: OS/2 and Windows Metrics Table +``OS/2``: OS/2 and Windows Metrics table ---------------------------------------- +The ``OS/2`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.O_S_2f_2 - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/OpenType_related.rst b/Doc/source/ttLib/tables/OpenType_related.rst new file mode 100644 index 0000000000..608d1f0d0f --- /dev/null +++ b/Doc/source/ttLib/tables/OpenType_related.rst @@ -0,0 +1,46 @@ +############################# +OpenType-table helper modules +############################# + +.. contents:: On this page: + :local: + + +.. rubric:: Overview: + :heading-level: 2 + +The OpenType-table helper modules documented on this page provide +support for OpenType's common table (and subtable) data formats. + +Most users should not need to access these modules directly. + + +fontTools.ttLib.tables.otTables +------------------------------- + +.. automodule:: fontTools.ttLib.tables.otTables + :members: + :undoc-members: + + +fontTools.ttLib.tables.otData +----------------------------- + +.. automodule:: fontTools.ttLib.tables.otData + :members: + :undoc-members: + + +fontTools.ttLib.tables.otConverters +----------------------------------- + +.. automodule:: fontTools.ttLib.tables.otConverters + :members: + :undoc-members: + + +fontTools.ttLib.tables.otTraverse +--------------------------------- +.. automodule:: fontTools.ttLib.tables.otTraverse + :members: + :undoc-members: diff --git a/Doc/source/ttLib/tables/S_I_N_G_.rst b/Doc/source/ttLib/tables/S_I_N_G_.rst index f7060120e3..9b42cb3612 100644 --- a/Doc/source/ttLib/tables/S_I_N_G_.rst +++ b/Doc/source/ttLib/tables/S_I_N_G_.rst @@ -1,7 +1,8 @@ -``SING``: SING Glyphlet Basic Information Table +``SING``: SING Glyphlet Basic Information table ----------------------------------------------- +The ``SING`` table is an Adobe Glyphlets table. + .. automodule:: fontTools.ttLib.tables.S_I_N_G_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/S_T_A_T_.rst b/Doc/source/ttLib/tables/S_T_A_T_.rst index 6aa96f84a4..cfc3285cfa 100644 --- a/Doc/source/ttLib/tables/S_T_A_T_.rst +++ b/Doc/source/ttLib/tables/S_T_A_T_.rst @@ -1,7 +1,8 @@ -``STAT``: Style Attributes Table +``STAT``: Style Attributes table -------------------------------- +The ``STAT`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.S_T_A_T_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/S_V_G_.rst b/Doc/source/ttLib/tables/S_V_G_.rst index bd96f15ff0..3b611f8e51 100644 --- a/Doc/source/ttLib/tables/S_V_G_.rst +++ b/Doc/source/ttLib/tables/S_V_G_.rst @@ -1,8 +1,9 @@ -``SVG``: SVG (Scalable Vector Graphics) Table +``SVG``: SVG (Scalable Vector Graphics) table --------------------------------------------- +The ``SVG`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.S_V_G_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/S__i_l_f.rst b/Doc/source/ttLib/tables/S__i_l_f.rst index cccda7250f..695235c5ac 100644 --- a/Doc/source/ttLib/tables/S__i_l_f.rst +++ b/Doc/source/ttLib/tables/S__i_l_f.rst @@ -1,7 +1,8 @@ -``Silf``: Graphite Rules Table +``Silf``: Graphite Rules table ------------------------------ +The ``Silf`` table is a Graphite table. + .. automodule:: fontTools.ttLib.tables.S__i_l_f - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/S__i_l_l.rst b/Doc/source/ttLib/tables/S__i_l_l.rst index 997ebf70b3..adf8cfb418 100644 --- a/Doc/source/ttLib/tables/S__i_l_l.rst +++ b/Doc/source/ttLib/tables/S__i_l_l.rst @@ -1,7 +1,8 @@ -``Sill``: Graphite Languages Table +``Sill``: Graphite Languages table ---------------------------------- +The ``Sill`` table is a Graphite table. + .. automodule:: fontTools.ttLib.tables.S__i_l_l - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/T_T_F_A_.rst b/Doc/source/ttLib/tables/T_T_F_A_.rst index 94a2d260f7..0937cbdf7c 100644 --- a/Doc/source/ttLib/tables/T_T_F_A_.rst +++ b/Doc/source/ttLib/tables/T_T_F_A_.rst @@ -1,8 +1,9 @@ -``TTFA``: ``ttfautohint`` Parameter Table +``TTFA``: ttfautohint Parameter table ----------------------------------------- +The ``TTFA`` table is used by the ``ttfautohint`` hinting program. + .. automodule:: fontTools.ttLib.tables.T_T_F_A_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/TupleVariation.rst b/Doc/source/ttLib/tables/TupleVariation.rst new file mode 100644 index 0000000000..5e267e13bf --- /dev/null +++ b/Doc/source/ttLib/tables/TupleVariation.rst @@ -0,0 +1,18 @@ +################################# +OpenType variations helper module +################################# + +.. currentmodule:: fontTools.ttLib.tables.TupleVariation + +.. rubric:: Overview: + :heading-level: 2 + +The :mod:`fontTools.ttLib.tables.TupleVariation` module is a helper for +:mod:`fontTools.ttLib` that implements lower-level support for +variable-font table converters. + + + +.. automodule:: fontTools.ttLib.tables.TupleVariation + :members: + :undoc-members: diff --git a/Doc/source/ttLib/tables/VTT_related.rst b/Doc/source/ttLib/tables/VTT_related.rst index 4265acddf2..1e82dbe495 100644 --- a/Doc/source/ttLib/tables/VTT_related.rst +++ b/Doc/source/ttLib/tables/VTT_related.rst @@ -1,11 +1,17 @@ -Visual TrueType Private Tables -============================== +``VTT*`` Visual TrueType private tables +======================================= + +The tables listed on this page are used by Microsoft's Visual TrueType +application. + +.. contents:: On this page: + :local: + ``TSI0``: Glyph Program Text Indices ------------------------------------ .. automodule:: fontTools.ttLib.tables.T_S_I__0 - :inherited-members: :members: :undoc-members: @@ -13,7 +19,6 @@ Visual TrueType Private Tables -------------------------------------------- .. automodule:: fontTools.ttLib.tables.T_S_I__1 - :inherited-members: :members: :undoc-members: @@ -21,7 +26,6 @@ Visual TrueType Private Tables ------------------------------- .. automodule:: fontTools.ttLib.tables.T_S_I__2 - :inherited-members: :members: :undoc-members: @@ -29,7 +33,6 @@ Visual TrueType Private Tables ---------------------------------- .. automodule:: fontTools.ttLib.tables.T_S_I__3 - :inherited-members: :members: :undoc-members: @@ -37,15 +40,13 @@ Visual TrueType Private Tables ---------------------------------- .. automodule:: fontTools.ttLib.tables.T_S_I__5 - :inherited-members: :members: :undoc-members: -``TSIB`` --------- +``TSIB``: VTT BASE Table Text Source +------------------------------------ .. automodule:: fontTools.ttLib.tables.T_S_I_B_ - :inherited-members: :members: :undoc-members: @@ -53,7 +54,6 @@ Visual TrueType Private Tables ----------------------------------------- .. automodule:: fontTools.ttLib.tables.T_S_I_C_ - :inherited-members: :members: :undoc-members: @@ -61,7 +61,6 @@ Visual TrueType Private Tables ------------------------------------ .. automodule:: fontTools.ttLib.tables.T_S_I_D_ - :inherited-members: :members: :undoc-members: @@ -69,7 +68,6 @@ Visual TrueType Private Tables ------------------------------------ .. automodule:: fontTools.ttLib.tables.T_S_I_J_ - :inherited-members: :members: :undoc-members: @@ -77,7 +75,6 @@ Visual TrueType Private Tables ------------------------------------ .. automodule:: fontTools.ttLib.tables.T_S_I_P_ - :inherited-members: :members: :undoc-members: @@ -85,7 +82,6 @@ Visual TrueType Private Tables ------------------------------------ .. automodule:: fontTools.ttLib.tables.T_S_I_S_ - :inherited-members: :members: :undoc-members: @@ -93,7 +89,6 @@ Visual TrueType Private Tables -------- .. automodule:: fontTools.ttLib.tables.T_S_I_V_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/V_D_M_X_.rst b/Doc/source/ttLib/tables/V_D_M_X_.rst index bf5fd67bde..9fff8e53d0 100644 --- a/Doc/source/ttLib/tables/V_D_M_X_.rst +++ b/Doc/source/ttLib/tables/V_D_M_X_.rst @@ -1,7 +1,8 @@ -``VDMX``: Vertical Device Metrics ---------------------------------- +``VDMX``: Vertical Device Metrics table +--------------------------------------- + +The ``VDMX`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables.V_D_M_X_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/V_O_R_G_.rst b/Doc/source/ttLib/tables/V_O_R_G_.rst index 0b8958aa7d..ce178664b5 100644 --- a/Doc/source/ttLib/tables/V_O_R_G_.rst +++ b/Doc/source/ttLib/tables/V_O_R_G_.rst @@ -1,8 +1,9 @@ -``VORG``: Vertical Origin Table +``VORG``: Vertical Origin table ------------------------------- +The ``VORG`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.V_O_R_G_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/V_V_A_R_.rst b/Doc/source/ttLib/tables/V_V_A_R_.rst index fce883173a..0951509226 100644 --- a/Doc/source/ttLib/tables/V_V_A_R_.rst +++ b/Doc/source/ttLib/tables/V_V_A_R_.rst @@ -1,8 +1,9 @@ -``VVAR``: Vertical Metrics Variations Table +``VVAR``: Vertical Metrics Variations table ------------------------------------------- +The ``VVAR`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables.V_V_A_R_ - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_a_n_k_r.rst b/Doc/source/ttLib/tables/_a_n_k_r.rst index 5541ac48e0..e7fbd01a5d 100644 --- a/Doc/source/ttLib/tables/_a_n_k_r.rst +++ b/Doc/source/ttLib/tables/_a_n_k_r.rst @@ -1,7 +1,8 @@ -``ankr``: Anchor Point Table +``ankr``: Anchor Point table ---------------------------- +The ``ankr`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._a_n_k_r - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_a_v_a_r.rst b/Doc/source/ttLib/tables/_a_v_a_r.rst index 81f2b31ceb..013f88570a 100644 --- a/Doc/source/ttLib/tables/_a_v_a_r.rst +++ b/Doc/source/ttLib/tables/_a_v_a_r.rst @@ -1,8 +1,9 @@ -``avar``: Axis Variations Table +``avar``: Axis Variations table ------------------------------- +The ``avar`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._a_v_a_r - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_b_s_l_n.rst b/Doc/source/ttLib/tables/_b_s_l_n.rst index a18fb82ed4..9a9fb4a6aa 100644 --- a/Doc/source/ttLib/tables/_b_s_l_n.rst +++ b/Doc/source/ttLib/tables/_b_s_l_n.rst @@ -1,7 +1,8 @@ ``bsln``: Baseline ------------------ +The ``bsln`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._b_s_l_n - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_c_i_d_g.rst b/Doc/source/ttLib/tables/_c_i_d_g.rst index f507ebd983..6406c50778 100644 --- a/Doc/source/ttLib/tables/_c_i_d_g.rst +++ b/Doc/source/ttLib/tables/_c_i_d_g.rst @@ -1,7 +1,8 @@ ``cidg``: CID to Glyph ID table ------------------------------- +The ``cidg`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._c_i_d_g - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_c_m_a_p.rst b/Doc/source/ttLib/tables/_c_m_a_p.rst index 4324815440..2cd1e2cfe5 100644 --- a/Doc/source/ttLib/tables/_c_m_a_p.rst +++ b/Doc/source/ttLib/tables/_c_m_a_p.rst @@ -1,6 +1,8 @@ -``cmap``: Character to Glyph Index Mapping Table +``cmap``: Character to Glyph Index Mapping table ------------------------------------------------ +The ``cmap`` table is an OpenType table. + .. autoclass:: fontTools.ttLib.tables._c_m_a_p.table__c_m_a_p .. autoclass:: fontTools.ttLib.tables._c_m_a_p.CmapSubtable diff --git a/Doc/source/ttLib/tables/_c_v_a_r.rst b/Doc/source/ttLib/tables/_c_v_a_r.rst index 6ae1b0d20e..8787dfa5b7 100644 --- a/Doc/source/ttLib/tables/_c_v_a_r.rst +++ b/Doc/source/ttLib/tables/_c_v_a_r.rst @@ -1,8 +1,9 @@ -``cvar``: CVT Variations Table +``cvar``: CVT Variations table ------------------------------ +The ``cvar`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._c_v_a_r - :inherited-members: :members: :undoc-members: @@ -12,6 +13,5 @@ TupleVariation .. automodule:: fontTools.ttLib.tables.TupleVariation :noindex: - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_c_v_t.rst b/Doc/source/ttLib/tables/_c_v_t.rst index ce02c563dd..6c23c84106 100644 --- a/Doc/source/ttLib/tables/_c_v_t.rst +++ b/Doc/source/ttLib/tables/_c_v_t.rst @@ -1,8 +1,9 @@ ``cvt``: Control Value Table ----------------------------- +The ``cvt`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._c_v_t - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_f_e_a_t.rst b/Doc/source/ttLib/tables/_f_e_a_t.rst index e32d36c025..f5414c7e20 100644 --- a/Doc/source/ttLib/tables/_f_e_a_t.rst +++ b/Doc/source/ttLib/tables/_f_e_a_t.rst @@ -1,7 +1,8 @@ ``feat``: Feature name table ---------------------------- +The ``feat`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._f_e_a_t - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_f_p_g_m.rst b/Doc/source/ttLib/tables/_f_p_g_m.rst index 165cb4ca02..a99c492bbe 100644 --- a/Doc/source/ttLib/tables/_f_p_g_m.rst +++ b/Doc/source/ttLib/tables/_f_p_g_m.rst @@ -1,7 +1,8 @@ -``fpgm``: Font Program ----------------------- +``fpgm``: Font Program table +---------------------------- + +The ``fpgm`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables._f_p_g_m - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_f_v_a_r.rst b/Doc/source/ttLib/tables/_f_v_a_r.rst index af83b3e2b9..ec729b487b 100644 --- a/Doc/source/ttLib/tables/_f_v_a_r.rst +++ b/Doc/source/ttLib/tables/_f_v_a_r.rst @@ -1,8 +1,9 @@ -``fvar``: Font Variations Table +``fvar``: Font Variations table ------------------------------- +The ``fvar`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._f_v_a_r - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_g_a_s_p.rst b/Doc/source/ttLib/tables/_g_a_s_p.rst index 628d155cb7..379f5e341c 100644 --- a/Doc/source/ttLib/tables/_g_a_s_p.rst +++ b/Doc/source/ttLib/tables/_g_a_s_p.rst @@ -1,8 +1,9 @@ -``gasp``: Grid-fitting and Scan-conversion Procedure Table +``gasp``: Grid-fitting and Scan-conversion Procedure table ---------------------------------------------------------- +The ``gasp`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._g_a_s_p - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_g_c_i_d.rst b/Doc/source/ttLib/tables/_g_c_i_d.rst index d7cbcf513c..0a86a18bbe 100644 --- a/Doc/source/ttLib/tables/_g_c_i_d.rst +++ b/Doc/source/ttLib/tables/_g_c_i_d.rst @@ -1,7 +1,8 @@ ``gcid``: Glyph ID to CID table ------------------------------- +The ``gcid`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._g_c_i_d - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_g_l_y_f.rst b/Doc/source/ttLib/tables/_g_l_y_f.rst index e3099cbfbc..9e2829a685 100644 --- a/Doc/source/ttLib/tables/_g_l_y_f.rst +++ b/Doc/source/ttLib/tables/_g_l_y_f.rst @@ -1,5 +1,7 @@ -``glyf``: Glyph Data --------------------- +``glyf``: Glyph Data table +-------------------------- + +The ``glyf`` table is an OpenType table. .. autoclass:: fontTools.ttLib.tables._g_l_y_f.table__g_l_y_f :members: diff --git a/Doc/source/ttLib/tables/_g_v_a_r.rst b/Doc/source/ttLib/tables/_g_v_a_r.rst index d7918a9160..3f148683e7 100644 --- a/Doc/source/ttLib/tables/_g_v_a_r.rst +++ b/Doc/source/ttLib/tables/_g_v_a_r.rst @@ -1,8 +1,9 @@ -``gvar``: Glyph Variations Table +``gvar``: Glyph Variations table --------------------------------- +The ``gvar`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._g_v_a_r - :inherited-members: :members: :undoc-members: @@ -11,6 +12,5 @@ TupleVariation ^^^^^^^^^^^^^^ .. automodule:: fontTools.ttLib.tables.TupleVariation - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_h_d_m_x.rst b/Doc/source/ttLib/tables/_h_d_m_x.rst index d18e1621a8..a54dee9c80 100644 --- a/Doc/source/ttLib/tables/_h_d_m_x.rst +++ b/Doc/source/ttLib/tables/_h_d_m_x.rst @@ -1,8 +1,9 @@ -``hdmx``: Horizontal Device Metrics ------------------------------------ +``hdmx``: Horizontal Device Metrics table +----------------------------------------- + +The ``hdmx`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables._h_d_m_x - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_h_e_a_d.rst b/Doc/source/ttLib/tables/_h_e_a_d.rst index daacebd4dd..d07686e4ee 100644 --- a/Doc/source/ttLib/tables/_h_e_a_d.rst +++ b/Doc/source/ttLib/tables/_h_e_a_d.rst @@ -1,8 +1,9 @@ -``head``: Font Header Table +``head``: Font Header table --------------------------- +The ``head`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._h_e_a_d - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_h_h_e_a.rst b/Doc/source/ttLib/tables/_h_h_e_a.rst index 76ba036042..8e70dd44ac 100644 --- a/Doc/source/ttLib/tables/_h_h_e_a.rst +++ b/Doc/source/ttLib/tables/_h_h_e_a.rst @@ -1,8 +1,9 @@ -``hhea``: Horizontal Header Table +``hhea``: Horizontal Header table --------------------------------- +The ``hhea`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._h_h_e_a - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_h_m_t_x.rst b/Doc/source/ttLib/tables/_h_m_t_x.rst index 523480fe4e..bad03e1c02 100644 --- a/Doc/source/ttLib/tables/_h_m_t_x.rst +++ b/Doc/source/ttLib/tables/_h_m_t_x.rst @@ -1,8 +1,9 @@ -``hmtx``: Horizontal Metrics Table +``hmtx``: Horizontal Metrics table ---------------------------------- +The ``hmtx`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._h_m_t_x - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_k_e_r_n.rst b/Doc/source/ttLib/tables/_k_e_r_n.rst index 87dae97d9b..b697d9c9ae 100644 --- a/Doc/source/ttLib/tables/_k_e_r_n.rst +++ b/Doc/source/ttLib/tables/_k_e_r_n.rst @@ -1,8 +1,9 @@ -``kern``: Kerning ------------------ +``kern``: Kerning table +----------------------- + +The ``kern`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables._k_e_r_n - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_l_c_a_r.rst b/Doc/source/ttLib/tables/_l_c_a_r.rst index d918b92156..c389dd9486 100644 --- a/Doc/source/ttLib/tables/_l_c_a_r.rst +++ b/Doc/source/ttLib/tables/_l_c_a_r.rst @@ -1,8 +1,9 @@ ``lcar``: Ligature Caret Table ------------------------------ +The ``lcar`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._l_c_a_r - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_l_o_c_a.rst b/Doc/source/ttLib/tables/_l_o_c_a.rst index cbff5d3194..8afe564ad7 100644 --- a/Doc/source/ttLib/tables/_l_o_c_a.rst +++ b/Doc/source/ttLib/tables/_l_o_c_a.rst @@ -1,8 +1,9 @@ -``loca``: Index to Location ---------------------------- +``loca``: Index to Location table +--------------------------------- + +The ``loca`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables._l_o_c_a - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_l_t_a_g.rst b/Doc/source/ttLib/tables/_l_t_a_g.rst index 955aaebe94..fb36c6b110 100644 --- a/Doc/source/ttLib/tables/_l_t_a_g.rst +++ b/Doc/source/ttLib/tables/_l_t_a_g.rst @@ -1,8 +1,9 @@ -``ltag``: Language Tag ----------------------- +``ltag``: Language Tag table +---------------------------- + +The ``ltag`` table is an Apple Advanced Typography (AAT) table. .. automodule:: fontTools.ttLib.tables._l_t_a_g - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_m_a_x_p.rst b/Doc/source/ttLib/tables/_m_a_x_p.rst index 1825d011b8..af0098aeb0 100644 --- a/Doc/source/ttLib/tables/_m_a_x_p.rst +++ b/Doc/source/ttLib/tables/_m_a_x_p.rst @@ -1,8 +1,9 @@ -``maxp``: Maximum Profile -------------------------- +``maxp``: Maximum Profile table +------------------------------- + +The ``maxp`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables._m_a_x_p - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_m_e_t_a.rst b/Doc/source/ttLib/tables/_m_e_t_a.rst index 61f8f47f0c..1a3d611157 100644 --- a/Doc/source/ttLib/tables/_m_e_t_a.rst +++ b/Doc/source/ttLib/tables/_m_e_t_a.rst @@ -1,8 +1,9 @@ -``meta``: Metadata Table +``meta``: Metadata table ------------------------ +The ``meta`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._m_e_t_a - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_m_o_r_t.rst b/Doc/source/ttLib/tables/_m_o_r_t.rst index 34ce57e95c..34fe47b362 100644 --- a/Doc/source/ttLib/tables/_m_o_r_t.rst +++ b/Doc/source/ttLib/tables/_m_o_r_t.rst @@ -1,8 +1,9 @@ ``mort``: Glyph Metamorphosis Table ----------------------------------- +The ``mort`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._m_o_r_t - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_m_o_r_x.rst b/Doc/source/ttLib/tables/_m_o_r_x.rst index 197ba16c94..db2d0addab 100644 --- a/Doc/source/ttLib/tables/_m_o_r_x.rst +++ b/Doc/source/ttLib/tables/_m_o_r_x.rst @@ -1,8 +1,9 @@ ``morx``: Extended Glyph Metamorphosis Table -------------------------------------------- +The ``morx`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._m_o_r_x - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_n_a_m_e.rst b/Doc/source/ttLib/tables/_n_a_m_e.rst index b500a461b3..c083c48c99 100644 --- a/Doc/source/ttLib/tables/_n_a_m_e.rst +++ b/Doc/source/ttLib/tables/_n_a_m_e.rst @@ -1,8 +1,9 @@ -``name``: Naming Table +``name``: Naming table ---------------------- +The ``name`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._n_a_m_e - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_o_p_b_d.rst b/Doc/source/ttLib/tables/_o_p_b_d.rst index 39d947b3b3..26265461e2 100644 --- a/Doc/source/ttLib/tables/_o_p_b_d.rst +++ b/Doc/source/ttLib/tables/_o_p_b_d.rst @@ -1,8 +1,9 @@ ``opbd``: Optical Bounds Table ------------------------------ +The ``opbd`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._o_p_b_d - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_p_o_s_t.rst b/Doc/source/ttLib/tables/_p_o_s_t.rst index 5035980e17..697181e7b4 100644 --- a/Doc/source/ttLib/tables/_p_o_s_t.rst +++ b/Doc/source/ttLib/tables/_p_o_s_t.rst @@ -1,8 +1,9 @@ -``post``: PostScript Table +``post``: PostScript table -------------------------- +The ``post`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._p_o_s_t - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_p_r_e_p.rst b/Doc/source/ttLib/tables/_p_r_e_p.rst index 1a538c5a7c..7c472ec63c 100644 --- a/Doc/source/ttLib/tables/_p_r_e_p.rst +++ b/Doc/source/ttLib/tables/_p_r_e_p.rst @@ -1,8 +1,9 @@ -``prep``: Control Value Program -------------------------------- +``prep``: Control Value Program table +------------------------------------- + +The ``prep`` table is an OpenType table. .. automodule:: fontTools.ttLib.tables._p_r_e_p - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_p_r_o_p.rst b/Doc/source/ttLib/tables/_p_r_o_p.rst index 638ba35081..2ccae74878 100644 --- a/Doc/source/ttLib/tables/_p_r_o_p.rst +++ b/Doc/source/ttLib/tables/_p_r_o_p.rst @@ -1,8 +1,9 @@ ``prop``: Glyph Properties Table -------------------------------- +The ``prop`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._p_r_o_p - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_s_b_i_x.rst b/Doc/source/ttLib/tables/_s_b_i_x.rst index 389cd53282..927dfe7d84 100644 --- a/Doc/source/ttLib/tables/_s_b_i_x.rst +++ b/Doc/source/ttLib/tables/_s_b_i_x.rst @@ -1,25 +1,12 @@ -``sbix``: Standard Bitmap Graphics Table +``sbix``: Standard Bitmap Graphics table ---------------------------------------- -.. automodule:: fontTools.ttLib.tables._s_b_i_x - :inherited-members: - :members: - :undoc-members: +The ``sbix`` table is an OpenType table. +This ``sbix`` table converter module depends on the +:mod:`.sbixGlyph` and :mod:`.sbixStrike` modules. -sbixGlyph -^^^^^^^^^ - -.. automodule:: fontTools.ttLib.tables.sbixGlyph - :inherited-members: - :members: - :undoc-members: - -sbixStrike -^^^^^^^^^^ - -.. automodule:: fontTools.ttLib.tables.sbixStrike - :inherited-members: +.. automodule:: fontTools.ttLib.tables._s_b_i_x :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_t_r_a_k.rst b/Doc/source/ttLib/tables/_t_r_a_k.rst index 34b454622b..3d648880db 100644 --- a/Doc/source/ttLib/tables/_t_r_a_k.rst +++ b/Doc/source/ttLib/tables/_t_r_a_k.rst @@ -1,8 +1,9 @@ ``trak``: Tracking table ------------------------ +The ``trak`` table is an Apple Advanced Typography (AAT) table. + .. automodule:: fontTools.ttLib.tables._t_r_a_k - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_v_h_e_a.rst b/Doc/source/ttLib/tables/_v_h_e_a.rst index d45d22617e..a1d0ac3ca7 100644 --- a/Doc/source/ttLib/tables/_v_h_e_a.rst +++ b/Doc/source/ttLib/tables/_v_h_e_a.rst @@ -1,8 +1,9 @@ -``vhea``: Vertical Header Table +``vhea``: Vertical Header table ------------------------------- +The ``vhea`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._v_h_e_a - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/_v_m_t_x.rst b/Doc/source/ttLib/tables/_v_m_t_x.rst index 21adb80c49..034df008ff 100644 --- a/Doc/source/ttLib/tables/_v_m_t_x.rst +++ b/Doc/source/ttLib/tables/_v_m_t_x.rst @@ -1,8 +1,9 @@ -``vmtx``: Vertical Metrics Table +``vmtx``: Vertical Metrics table -------------------------------- +The ``vmtx`` table is an OpenType table. + .. automodule:: fontTools.ttLib.tables._v_m_t_x - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttLib/tables/grUtils.rst b/Doc/source/ttLib/tables/grUtils.rst new file mode 100644 index 0000000000..d2ad899c41 --- /dev/null +++ b/Doc/source/ttLib/tables/grUtils.rst @@ -0,0 +1,17 @@ +############################ +Graphite table helper module +############################ + +.. rubric:: Overview: + :heading-level: 2 + +The :mod:`grUtils` module is a helper for :mod:`fontTools.ttLib` that +provides lower-level support for Graphite table converters. + +.. rubric:: Module members: + :heading-level: 2 + +.. automodule:: fontTools.ttLib.tables.grUtils + :inherited-members: + :members: + :undoc-members: diff --git a/Doc/source/ttLib/tables/table_api.rst b/Doc/source/ttLib/tables/table_api.rst new file mode 100644 index 0000000000..3fa8647cb5 --- /dev/null +++ b/Doc/source/ttLib/tables/table_api.rst @@ -0,0 +1,83 @@ +########################## +Base table classes and API +########################## + +.. contents:: On this page: + :local: + +.. rubric:: Overview: + :heading-level: 2 + +The modules documented on this page are the base classes on which the +:mod:`fontTools.ttLib` table converters are built. The +:class:`.DefaultTable` is the most general; :class:`.asciiTable` is a +simpler option for storing text-based data. For OpenType and TrueType +fonts, the :class:`.otBase.BaseTTXConverter` leverages the model used +by the majority of existing OpenType/TrueType converters. + + +Contributing your own table convertors +-------------------------------------- + +To add support for a new font table that fontTools does not currently implement, +you must subclass from :py:mod:`fontTools.ttLib.tables.DefaultTable.DefaultTable`. +It provides some default behavior, as well as a constructor method (``__init__``) +that you don't need to override. + +Your converter should minimally provide two methods:: + + + class table_F_O_O_(DefaultTable.DefaultTable): # converter for table 'FOO ' + + def decompile(self, data, ttFont): + # 'data' is the raw table data. Unpack it into a + # Python data structure. + # 'ttFont' is a ttLib.TTfile instance, enabling you to + # refer to other tables. Do ***not*** keep a reference to + # it: it will cause a circular reference (ttFont saves + # a reference to us), and that means we'll be leaking + # memory. If you need to use it in other methods, just + # pass it around as a method argument. + + def compile(self, ttFont): + # Return the raw data, as converted from the Python + # data structure. + # Again, 'ttFont' is there so you can access other tables. + # Same warning applies. + + +If you want to support TTX import/export as well, you need to provide two +additional methods:: + + + def toXML(self, writer, ttFont): + # XXX + + def fromXML(self, (name, attrs, content), ttFont): + # XXX + + + +fontTools.ttLib.tables.DefaultTable +----------------------------------- + +.. automodule:: fontTools.ttLib.tables.DefaultTable + :members: + :undoc-members: + + +fontTools.ttLib.tables.asciiTable +--------------------------------- + +.. automodule:: fontTools.ttLib.tables.asciiTable + :members: + :undoc-members: + + +fontTools.ttLib.tables.otBase +----------------------------- + +.. automodule:: fontTools.ttLib.tables.otBase + :members: + :undoc-members: + diff --git a/Doc/source/ttLib/tables/ttProgram.rst b/Doc/source/ttLib/tables/ttProgram.rst new file mode 100644 index 0000000000..091e22374a --- /dev/null +++ b/Doc/source/ttLib/tables/ttProgram.rst @@ -0,0 +1,16 @@ +################################################### +ttProgram: TrueType bytecode assembler/disassembler +################################################### + +.. rubric:: Overview: + :heading-level: 2 + +The :mod:`fontTools.ttLib.ttProgram` module is a helper for +:mod:`fontTools.ttLib`. + +.. automodule:: fontTools.ttLib.tables.ttProgram + :members: + :undoc-members: + + .. rubric:: Module members: + :heading-level: 2 diff --git a/Doc/source/ttLib/ttCollection.rst b/Doc/source/ttLib/ttCollection.rst index 0ca4ebd0dc..b19fcdbe84 100644 --- a/Doc/source/ttLib/ttCollection.rst +++ b/Doc/source/ttLib/ttCollection.rst @@ -1,8 +1,15 @@ -############ -ttCollection -############ +################################################################ +ttCollection: Access fonts within a TrueType/OpenType Collection +################################################################ + +.. rubric:: Overview + :heading-level: 2 + +The :mod:`fontTools.ttLib.ttCollection` module is a helper for +:mod:`fontTools.ttLib`. It supports the reading of TrueType and +OpenType `Collection `_ files (\*.ttc, \*.otc) so that each member font in +the collection is accessible as a :class:`.TTFont` instance. .. automodule:: fontTools.ttLib.ttCollection - :inherited-members: :members: - :undoc-members: \ No newline at end of file + :undoc-members: diff --git a/Doc/source/ttLib/ttFont.rst b/Doc/source/ttLib/ttFont.rst index a3b4c9d0ef..7c7a60da61 100644 --- a/Doc/source/ttLib/ttFont.rst +++ b/Doc/source/ttLib/ttFont.rst @@ -1,17 +1,99 @@ -############################################## -ttFont: Read/write OpenType and TrueType fonts -############################################## +#################################### +ttFont: Read and write font contents +#################################### + +.. contents:: On this page: + :local: + +.. rubric:: Overview + :heading-level: 2 + +:mod:`.ttLib.ttFont` is the primary fontTools interface for +inspecting, constructing, or deconstructing TrueType and OpenType +fonts. + +The :class:`fontTools.ttLib.ttFont.TTFont` class provides access to +font-level data, including font metrics, substitution and positioning +features, and metadata, through a set of :doc:`table converters +`. A :class:`.TTFont` may be instantiated from a single +font file, or it may be a member of a +:class:`.TTCollection`. :class:`.TTFont` objects can also be +constructed from scratch. + + +glyphSets and ttGlyphs +---------------------- + +In addition to font-wide data, :mod:`.ttLib.ttFont` provides access to +individual glyphs through a :class:`.TTFont` instance's +``.glyphSet[]`` attribute. A ``.glyphSet`` is a dict-like object that is +indexed by glyph names. Users can use the glyphSet to interact with +each glyph's contours, components, points, and glyph metrics. + +Informally, some fontTools code or documentation will make reference to +the individual glyphs in a ``.glyphSet`` as a "ttGlyph" or the +like. This is convenient terminology, particularly for +discussion. However, it is important to note that there is not a +"ttGlyph" class. Instead, the ``.glyphSet`` attribute of a +:class:`.TTFont` serves as an abstraction layer that provides a +uniform interface to the glyphs, regardless of whether the +:class:`.TTFont` instance in use comes from a font file with +TrueType-flavored glyphs (and, therefore, has a `glyf` table +containing glyph contours) or a font with PostScript-flavored outlines +(and, therefore, with a ``CFF`` or ``CFF2`` table containing the glyph +contours). + +Regardless of the flavor, each "ttGlyph" entry in the ``.glyphSet`` +includes the corresponding Bezier outlines and components from the +``glyf`` or ``CFF``/``CFF2`` table and the glyph's metrics. Horizontal +metrics are drawn from the font's ``hmtx`` table, and vertical metrics +(if any) are drawn from the ``vmtx`` table. These attributes are: + + width + The advance width of the glyph + + lsb + The left sidebearing of the glyph + + height + (For vertical-layout fonts) The advance height of the glyph + + tsb + (for vertical-layout fonts) The top sidebearing of the glyph + +Note that these attributes do not describe the bounding box of the +glyph filled shape, because the filled area might include negative +coordinate values or extend beyond the advance width due to overhang. + +The bounds of the glyph are accessible as ``xMin``, ``xMax``, +``yMin``, and ``yMax`` attributes. + +For implementation details regarding the different flavors of +"ttGlyph", see the :doc:`ttGlyphSet ` +documentation. + +These glyph objects also implement the :doc:`Pen Protocol +` by providing ``.draw()`` and ``.drawPoints()`` +methods. See the :doc:`pens ` package documenation +for more. + + +Package contents +---------------- + .. autoclass:: fontTools.ttLib.ttFont.TTFont - :inherited-members: :members: + :undoc-members: + + .. autoclass:: fontTools.ttLib.ttFont.GlyphOrder - :inherited-members: :members: :undoc-members: :private-members: + .. automodule:: fontTools.ttLib.ttFont :members: getTableModule, registerCustomTableClass, unregisterCustomTableClass, getCustomTableClass, getClassTag, newTable, tagToIdentifier, identifierToTag, tagToXML, xmlToTag, sortedTagList, reorderFontTables - + :exclude-members: TTFont, GlyphOrder diff --git a/Doc/source/ttLib/ttGlyphSet.rst b/Doc/source/ttLib/ttGlyphSet.rst new file mode 100644 index 0000000000..2bbfc73312 --- /dev/null +++ b/Doc/source/ttLib/ttGlyphSet.rst @@ -0,0 +1,18 @@ +################################################# +ttGlyphSet: GlyphSets returned by a TTFont object +################################################# + +.. rubric:: Overview + :heading-level: 2 + +The :mod:`fontTools.ttLib.ttGlyphSet` module is a helper for +:mod:`fontTools.ttLib`. + +Most users will not need to access this module directly. Instead, the +glyphs in a font are accessible as a ``.glyphSet`` dictionary in a +:class:`TTFont` instance. + +.. automodule:: fontTools.ttLib.ttGlyphSet + :members: + :undoc-members: + :private-members: diff --git a/Doc/source/ttLib/woff2.rst b/Doc/source/ttLib/woff2.rst index 327bb5b268..b7fcb29754 100644 --- a/Doc/source/ttLib/woff2.rst +++ b/Doc/source/ttLib/woff2.rst @@ -1,8 +1,11 @@ -##### -woff2 -##### +################################################ +woff2: Read and write the WOFF2 font file format +################################################ + +Note also that :mod:`woff2` supports some :doc:`optional ` +external libraries. + .. automodule:: fontTools.ttLib.woff2 - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ttx.rst b/Doc/source/ttx.rst index 9d30683b58..a711ef1c05 100644 --- a/Doc/source/ttx.rst +++ b/Doc/source/ttx.rst @@ -1,6 +1,6 @@ -### -ttx -### +################################## +ttx: Convert fonts to XML and back +################################## TTX – From OpenType and TrueType to XML and Back @@ -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 @@ -57,6 +57,5 @@ It is possible that different glyphs use the same name. If this happens, we forc Because the order in which glyphs are stored inside the binary font is important, we maintain an ordered list of glyph names in the font. .. automodule:: fontTools.ttx - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/converters.rst b/Doc/source/ufoLib/converters.rst index 2b37e56ac5..134be52d09 100644 --- a/Doc/source/ufoLib/converters.rst +++ b/Doc/source/ufoLib/converters.rst @@ -1,9 +1,8 @@ - -########## -converters -########## +####################################################### +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 ba079abd80..1896ce516d 100644 --- a/Doc/source/ufoLib/errors.rst +++ b/Doc/source/ufoLib/errors.rst @@ -1,9 +1,7 @@ - -###### -errors -###### +################################################### +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 33a3c1b78a..41ab4a0c27 100644 --- a/Doc/source/ufoLib/filenames.rst +++ b/Doc/source/ufoLib/filenames.rst @@ -1,9 +1,7 @@ - -######### -filenames -######### +########################################################################## +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 15bde5abe2..4e48e75f1b 100644 --- a/Doc/source/ufoLib/glifLib.rst +++ b/Doc/source/ufoLib/glifLib.rst @@ -1,9 +1,7 @@ - -####### -glifLib -####### +####################################### +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 514c5c9f77..2bc5ab2a0d 100644 --- a/Doc/source/ufoLib/index.rst +++ b/Doc/source/ufoLib/index.rst @@ -1,22 +1,49 @@ +################################################ +ufoLib: Read and write Unified Font Object files +################################################ -###### -ufoLib -###### - -.. toctree:: - :maxdepth: 1 - - converters - errors - filenames - glifLib - kerning - plistlib - pointpen - utils - validators +.. rubric:: Overview: + :heading-level: 2 .. automodule:: fontTools.ufoLib - :inherited-members: - :members: + :no-inherited-members: + :members: :undoc-members: + :member-order: bysource + + .. rubric:: Modules: + :heading-level: 2 + + ufoLib provides the following modules: + + .. toctree:: + :maxdepth: 1 + :titlesonly: + + converters + errors + filenames + glifLib + kerning + utils + validators + + 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: 2 + +.. autodata:: fontInfoAttributesVersion1 +.. autodata:: fontInfoAttributesVersion2 +.. autodata:: fontInfoAttributesVersion3 +.. autodata:: deprecatedFontInfoAttributesVersion2 diff --git a/Doc/source/ufoLib/kerning.rst b/Doc/source/ufoLib/kerning.rst index 4d9d0c150a..e16f993822 100644 --- a/Doc/source/ufoLib/kerning.rst +++ b/Doc/source/ufoLib/kerning.rst @@ -1,9 +1,7 @@ - -####### -kerning -####### +########################################### +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 d639947c38..eb919dce4d 100644 --- a/Doc/source/ufoLib/plistlib.rst +++ b/Doc/source/ufoLib/plistlib.rst @@ -1,9 +1,9 @@ +##################################################################### +plistlib: Support for reading and writing .plist files *[deprecated]* +##################################################################### -######## -plistlib -######## - -.. automodule:: fontTools.ufoLib.plistlib - :inherited-members: - :members: - :undoc-members: +.. important:: + + .. automodule:: fontTools.ufoLib.plistlib + :members: + :undoc-members: diff --git a/Doc/source/ufoLib/pointpen.rst b/Doc/source/ufoLib/pointpen.rst index 5fb8c1cad5..0f11bcce0f 100644 --- a/Doc/source/ufoLib/pointpen.rst +++ b/Doc/source/ufoLib/pointpen.rst @@ -1,9 +1,9 @@ +###################################################################### +pointPen: A pen for accessing points in a glyph contour *[deprecated]* +###################################################################### -######## -pointPen -######## - -.. automodule:: fontTools.ufoLib.pointPen - :inherited-members: - :members: - :undoc-members: +.. important:: + + .. automodule:: fontTools.ufoLib.pointPen + :members: + :undoc-members: diff --git a/Doc/source/ufoLib/utils.rst b/Doc/source/ufoLib/utils.rst index d5ab143ec3..2a63e3812a 100644 --- a/Doc/source/ufoLib/utils.rst +++ b/Doc/source/ufoLib/utils.rst @@ -1,9 +1,7 @@ - -##### -utils -##### +######################################### +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 363d8648a0..2288378229 100644 --- a/Doc/source/ufoLib/validators.rst +++ b/Doc/source/ufoLib/validators.rst @@ -1,9 +1,7 @@ - -########## -validators -########## +##################################### +validators: Data-validation functions +##################################### .. automodule:: fontTools.ufoLib.validators - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/unicode.rst b/Doc/source/unicode.rst index 65481d58f6..228e7d5063 100644 --- a/Doc/source/unicode.rst +++ b/Doc/source/unicode.rst @@ -1,8 +1,20 @@ +:orphan: + ####### unicode ####### +.. rubric:: Overview: + :heading-level: 2 + +The :mod:`fonttools.unicode` module is a helper for +:mod:`fontTools.ttx` that provides access to Unicode data and manages +fallback support. + +Note also that :mod:`unicode` supports some :doc:`optional ` +external libraries. + + .. automodule:: fontTools.unicode - :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 1ae4a1b5ee..f8f5e2eef6 100644 --- a/Doc/source/varLib/index.rst +++ b/Doc/source/varLib/index.rst @@ -1,10 +1,24 @@ -################################## -varLib: OpenType Variation Support -################################## +####################################### +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 @@ -21,10 +35,6 @@ varLib: OpenType Variation Support 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 ----------- @@ -109,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 00d067a293..aa666c9682 100644 --- a/Doc/source/voltLib/index.rst +++ b/Doc/source/voltLib/index.rst @@ -1,34 +1,78 @@ -#################################### -voltLib: Read/write MS VOLT projects -#################################### +######################################## +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 733d28badc..ed0fd36c35 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -3,6 +3,6 @@ log = logging.getLogger(__name__) -version = __version__ = "4.52.2" +version = __version__ = "4.58.1.dev0" __all__ = ["version", "log", "configLogger"] diff --git a/Lib/fontTools/cffLib/CFF2ToCFF.py b/Lib/fontTools/cffLib/CFF2ToCFF.py index 689412ce2b..f929cc9686 100644 --- a/Lib/fontTools/cffLib/CFF2ToCFF.py +++ b/Lib/fontTools/cffLib/CFF2ToCFF.py @@ -32,9 +32,10 @@ def _convertCFF2ToCFF(cff, otFont): cff.major = 1 - topDictData = TopDictIndex(None, isCFF2=True) + topDictData = TopDictIndex(None) for item in cff.topDictIndex: # Iterate over, such that all are decompiled + item.cff2GetGlyphOrder = None topDictData.append(item) cff.topDictIndex = topDictData topDict = topDictData[0] @@ -99,6 +100,21 @@ def _convertCFF2ToCFF(cff, otFont): if width != private.defaultWidthX: cs.program.insert(0, width - private.nominalWidthX) + mapping = { + name: ("cid" + str(n) if n else ".notdef") + for n, name in enumerate(topDict.charset) + } + topDict.charset = [ + "cid" + str(n) if n else ".notdef" for n in range(len(topDict.charset)) + ] + charStrings.charStrings = { + mapping[name]: v for name, v in charStrings.charStrings.items() + } + + # I'm not sure why the following is *not* necessary. And it breaks + # the output if I add it. + # topDict.ROS = ("Adobe", "Identity", 0) + def convertCFF2ToCFF(font, *, updatePostTable=True): cff = font["CFF2"].cff diff --git a/Lib/fontTools/cffLib/CFFToCFF2.py b/Lib/fontTools/cffLib/CFFToCFF2.py index 37463a5b9b..2555f0b242 100644 --- a/Lib/fontTools/cffLib/CFFToCFF2.py +++ b/Lib/fontTools/cffLib/CFFToCFF2.py @@ -81,7 +81,7 @@ def _convertCFFToCFF2(cff, otFont): thisLocalSubrs = ( localSubrs[fdIndex] - if fdIndex + if fdIndex is not None else ( getattr(topDict.Private, "Subrs", []) if hasattr(topDict, "Private") @@ -104,9 +104,10 @@ def _convertCFFToCFF2(cff, otFont): # just pop the first number since it may be a subroutine call. # Instead, when seeing that, we embed the subroutine and recurse. # If this ever happened, we later prune unused subroutines. - while program[1] in ["callsubr", "callgsubr"]: + while len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]: removeUnusedSubrs = True subrNumber = program.pop(0) + assert isinstance(subrNumber, int), subrNumber op = program.pop(0) bias = extractor.localBias if op == "callsubr" else extractor.globalBias subrNumber += bias @@ -114,6 +115,7 @@ def _convertCFFToCFF2(cff, otFont): subrProgram = subrSet[subrNumber].program program[:0] = subrProgram # Now pop the actual width + assert len(program) >= 1, program program.pop(0) if program and program[-1] == "endchar": diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index c192ec77af..4ad724a27a 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1,7 +1,7 @@ """cffLib: read/write Adobe CFF fonts -OpenType fonts with PostScript outlines contain a completely independent -font file, Adobe's *Compact Font Format*. So dealing with OpenType fonts +OpenType fonts with PostScript outlines embed a completely independent +font file in Adobe's *Compact Font Format*. So dealing with OpenType fonts requires also dealing with CFF. This module allows you to read and write fonts written in the CFF format. @@ -867,7 +867,11 @@ def decompile(self): if self.file: # read data in from file. Assume position is correct. length = readCard16(self.file) - self.data = self.file.read(length) + # https://github.com/fonttools/fonttools/issues/3673 + if length == 65535: + self.data = self.file.read() + else: + self.data = self.file.read(length) globalState = {} reader = OTTableReader(self.data, globalState) self.otVarStore = ot.VarStore() @@ -1460,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 @@ -1659,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": @@ -1715,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 @@ -1956,7 +1995,8 @@ def __init__(self, varStoreData, parent): self.parent = parent if not varStoreData.data: varStoreData.compile() - data = [packCard16(len(varStoreData.data)), varStoreData.data] + varStoreDataLen = min(0xFFFF, len(varStoreData.data)) + data = [packCard16(varStoreDataLen), varStoreData.data] self.data = bytesjoin(data) def setPos(self, pos, endPos): @@ -2281,7 +2321,7 @@ def arg_delta_blend(self, value): # For PrivateDict BlueValues, the default font # values are absolute, not relative. # Must convert these back to relative coordinates - # befor writing to CFF2. + # before writing to CFF2. defaultValue = value[i][0] firstList[i] = defaultValue - prevVal prevVal = defaultValue diff --git a/Lib/fontTools/cffLib/specializer.py b/Lib/fontTools/cffLib/specializer.py index bb7f89e4ff..5fddcb67dd 100644 --- a/Lib/fontTools/cffLib/specializer.py +++ b/Lib/fontTools/cffLib/specializer.py @@ -80,8 +80,9 @@ def programToCommands(program, getNumRegions=None): numBlendArgs = numBlends * numSourceFonts + 1 # replace first blend op by a list of the blend ops. stack[-numBlendArgs:] = [stack[-numBlendArgs:]] - lenBlendStack += numBlends + len(stack) - 1 - lastBlendIndex = len(stack) + lenStack = len(stack) + lenBlendStack += numBlends + lenStack - 1 + lastBlendIndex = lenStack # if a blend op exists, this is or will be a CFF2 charstring. continue @@ -153,9 +154,10 @@ def commandsToProgram(commands): def _everyN(el, n): """Group the list el into groups of size n""" - if len(el) % n != 0: + l = len(el) + if l % n != 0: raise ValueError(el) - for i in range(0, len(el), n): + for i in range(0, l, n): yield el[i : i + n] @@ -218,9 +220,10 @@ def rrcurveto(args): @staticmethod def hhcurveto(args): - if len(args) < 4 or len(args) % 4 > 1: + l = len(args) + if l < 4 or l % 4 > 1: raise ValueError(args) - if len(args) % 2 == 1: + if l % 2 == 1: yield ("rrcurveto", [args[1], args[0], args[2], args[3], args[4], 0]) args = args[5:] for args in _everyN(args, 4): @@ -228,9 +231,10 @@ def hhcurveto(args): @staticmethod def vvcurveto(args): - if len(args) < 4 or len(args) % 4 > 1: + l = len(args) + if l < 4 or l % 4 > 1: raise ValueError(args) - if len(args) % 2 == 1: + if l % 2 == 1: yield ("rrcurveto", [args[0], args[1], args[2], args[3], 0, args[4]]) args = args[5:] for args in _everyN(args, 4): @@ -238,11 +242,12 @@ def vvcurveto(args): @staticmethod def hvcurveto(args): - if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: + l = len(args) + if l < 4 or l % 8 not in {0, 1, 4, 5}: raise ValueError(args) last_args = None - if len(args) % 2 == 1: - lastStraight = len(args) % 8 == 5 + if l % 2 == 1: + lastStraight = l % 8 == 5 args, last_args = args[:-5], args[-5:] it = _everyN(args, 4) try: @@ -262,11 +267,12 @@ def hvcurveto(args): @staticmethod def vhcurveto(args): - if len(args) < 4 or len(args) % 8 not in {0, 1, 4, 5}: + l = len(args) + if l < 4 or l % 8 not in {0, 1, 4, 5}: raise ValueError(args) last_args = None - if len(args) % 2 == 1: - lastStraight = len(args) % 8 == 5 + if l % 2 == 1: + lastStraight = l % 8 == 5 args, last_args = args[:-5], args[-5:] it = _everyN(args, 4) try: @@ -286,7 +292,8 @@ def vhcurveto(args): @staticmethod def rcurveline(args): - if len(args) < 8 or len(args) % 6 != 2: + l = len(args) + if l < 8 or l % 6 != 2: raise ValueError(args) args, last_args = args[:-2], args[-2:] for args in _everyN(args, 6): @@ -295,7 +302,8 @@ def rcurveline(args): @staticmethod def rlinecurve(args): - if len(args) < 8 or len(args) % 2 != 0: + l = len(args) + if l < 8 or l % 2 != 0: raise ValueError(args) args, last_args = args[:-6], args[-6:] for args in _everyN(args, 2): @@ -330,8 +338,9 @@ def _convertBlendOpToArgs(blendList): # comprehension. See calling context args = args[:-1] - numRegions = len(args) // numBlends - 1 - if not (numBlends * (numRegions + 1) == len(args)): + l = len(args) + numRegions = l // numBlends - 1 + if not (numBlends * (numRegions + 1) == l): raise ValueError(blendList) defaultArgs = [[arg] for arg in args[:numBlends]] @@ -368,7 +377,7 @@ def generalizeCommands(commands, ignoreErrors=False): raise func = getattr(mapping, op, None) - if not func: + if func is None: result.append((op, args)) continue try: @@ -446,9 +455,9 @@ def _convertToBlendCmds(args): i = 0 while i < num_args: arg = args[i] + i += 1 if not isinstance(arg, list): new_args.append(arg) - i += 1 stack_use += 1 else: prev_stack_use = stack_use @@ -458,21 +467,26 @@ def _convertToBlendCmds(args): # up to the max stack limit. num_sources = len(arg) - 1 blendlist = [arg] - i += 1 stack_use += 1 + num_sources # 1 for the num_blends arg - while (i < num_args) and isinstance(args[i], list): + + # if we are here, max stack is the CFF2 max stack. + # I use the CFF2 max stack limit here rather than + # the 'maxstack' chosen by the client, as the default + # maxstack may have been used unintentionally. For all + # the other operators, this just produces a little less + # optimization, but here it puts a hard (and low) limit + # on the number of source fonts that can be used. + # + # Make sure the stack depth does not exceed (maxstack - 1), so + # that subroutinizer can insert subroutine calls at any point. + while ( + (i < num_args) + and isinstance(args[i], list) + and stack_use + num_sources < maxStackLimit + ): blendlist.append(args[i]) i += 1 stack_use += num_sources - if stack_use + num_sources > maxStackLimit: - # if we are here, max stack is the CFF2 max stack. - # I use the CFF2 max stack limit here rather than - # the 'maxstack' chosen by the client, as the default - # maxstack may have been used unintentionally. For all - # the other operators, this just produces a little less - # optimization, but here it puts a hard (and low) limit - # on the number of source fonts that can be used. - break # blendList now contains as many single blend tuples as can be # combined without exceeding the CFF2 stack limit. num_blends = len(blendlist) @@ -504,6 +518,19 @@ def _addArgs(a, b): return a + b +def _argsStackUse(args): + stackLen = 0 + maxLen = 0 + for arg in args: + if type(arg) is list: + # Blended arg + maxLen = max(maxLen, stackLen + _argsStackUse(arg)) + stackLen += arg[-1] + else: + stackLen += 1 + return max(stackLen, maxLen) + + def specializeCommands( commands, ignoreErrors=False, @@ -697,6 +724,7 @@ def specializeCommands( continue # 5. Combine adjacent operators when possible, minding not to go over max stack size. + stackUse = _argsStackUse(commands[-1][1]) if commands else 0 for i in range(len(commands) - 1, 0, -1): op1, args1 = commands[i - 1] op2, args2 = commands[i] @@ -707,9 +735,10 @@ def specializeCommands( if op1 == op2: new_op = op1 else: - if op2 == "rrcurveto" and len(args2) == 6: + l = len(args2) + if op2 == "rrcurveto" and l == 6: new_op = "rlinecurve" - elif len(args2) == 2: + elif l == 2: new_op = "rcurveline" elif (op1, op2) in {("rlineto", "rlinecurve"), ("rrcurveto", "rcurveline")}: @@ -746,9 +775,14 @@ def specializeCommands( # Make sure the stack depth does not exceed (maxstack - 1), so # that subroutinizer can insert subroutine calls at any point. - if new_op and len(args1) + len(args2) < maxstack: + args1StackUse = _argsStackUse(args1) + combinedStackUse = max(args1StackUse, len(args1) + stackUse) + if new_op and combinedStackUse < maxstack: commands[i - 1] = (new_op, args1 + args2) del commands[i] + stackUse = combinedStackUse + else: + stackUse = args1StackUse # 6. Resolve any remaining made-up operators into real operators. for i in range(len(commands)): @@ -759,9 +793,11 @@ def specializeCommands( continue if op[2:] == "curveto" and op[:2] not in {"rr", "hh", "vv", "vh", "hv"}: + l = len(args) + op0, op1 = op[:2] if (op0 == "r") ^ (op1 == "r"): - assert len(args) % 2 == 1 + assert l % 2 == 1 if op0 == "0": op0 = "h" if op1 == "0": @@ -772,9 +808,9 @@ def specializeCommands( op1 = _negateCategory(op0) assert {op0, op1} <= {"h", "v"}, (op0, op1) - if len(args) % 2: + if l % 2: if op0 != op1: # vhcurveto / hvcurveto - if (op0 == "h") ^ (len(args) % 8 == 1): + if (op0 == "h") ^ (l % 8 == 1): # Swap last two args order args = args[:-2] + args[-1:] + args[-2:-1] else: # hhcurveto / vvcurveto @@ -822,26 +858,67 @@ def specializeProgram(program, getNumRegions=None, **kwargs): default=None, help="Number of variable-font regions for blend opertaions.", ) + parser.add_argument( + "--font", + metavar="FONTFILE", + default=None, + help="CFF2 font to specialize.", + ) + parser.add_argument( + "-o", + "--output-file", + type=str, + help="Output font file name.", + ) options = parser.parse_args(sys.argv[1:]) - getNumRegions = ( - None - if options.num_regions is None - else lambda vsIndex: int(options.num_regions[0 if vsIndex is None else vsIndex]) - ) - - program = stringToProgram(options.program) - print("Program:") - print(programToString(program)) - commands = programToCommands(program, getNumRegions) - print("Commands:") - print(commands) - program2 = commandsToProgram(commands) - print("Program from commands:") - print(programToString(program2)) - assert program == program2 - print("Generalized program:") - print(programToString(generalizeProgram(program, getNumRegions))) - print("Specialized program:") - print(programToString(specializeProgram(program, getNumRegions))) + if options.program: + getNumRegions = ( + None + if options.num_regions is None + else lambda vsIndex: int( + options.num_regions[0 if vsIndex is None else vsIndex] + ) + ) + + program = stringToProgram(options.program) + print("Program:") + print(programToString(program)) + commands = programToCommands(program, getNumRegions) + print("Commands:") + print(commands) + program2 = commandsToProgram(commands) + print("Program from commands:") + print(programToString(program2)) + assert program == program2 + print("Generalized program:") + print(programToString(generalizeProgram(program, getNumRegions))) + print("Specialized program:") + print(programToString(specializeProgram(program, getNumRegions))) + + if options.font: + from fontTools.ttLib import TTFont + + font = TTFont(options.font) + cff2 = font["CFF2"].cff.topDictIndex[0] + charstrings = cff2.CharStrings + for glyphName in charstrings.keys(): + charstring = charstrings[glyphName] + charstring.decompile() + getNumRegions = charstring.private.getNumRegions + charstring.program = specializeProgram( + charstring.program, getNumRegions, maxstack=maxStackLimit + ) + + if options.output_file is None: + from fontTools.misc.cliTools import makeOutputFileName + + outfile = makeOutputFileName( + options.font, overWrite=True, suffix=".specialized" + ) + else: + outfile = options.output_file + if outfile: + print("Saving", outfile) + font.save(outfile) diff --git a/Lib/fontTools/cffLib/transforms.py b/Lib/fontTools/cffLib/transforms.py index 91f6999fe6..82c70f81f4 100644 --- a/Lib/fontTools/cffLib/transforms.py +++ b/Lib/fontTools/cffLib/transforms.py @@ -457,6 +457,13 @@ def remove_unused_subroutines(cff): if subrs == font.GlobalSubrs: if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"): local_subrs = font.Private.Subrs + elif ( + hasattr(font, "FDArray") + and len(font.FDArray) == 1 + and hasattr(font.FDArray[0].Private, "Subrs") + ): + # Technically we shouldn't do this. But I've run into fonts that do it. + local_subrs = font.FDArray[0].Private.Subrs else: local_subrs = None else: diff --git a/Lib/fontTools/config/__init__.py b/Lib/fontTools/config/__init__.py index 41ab8f7581..ff0328a3d4 100644 --- a/Lib/fontTools/config/__init__.py +++ b/Lib/fontTools/config/__init__.py @@ -73,3 +73,18 @@ class Config(AbstractConfig): parse=Option.parse_optional_bool, validate=Option.validate_optional_bool, ) + +Config.register_option( + name="fontTools.ttLib:OPTIMIZE_FONT_SPEED", + help=dedent( + """\ + Enable optimizations that prioritize speed over file size. This + mainly affects how glyf table and gvar / VARC tables are compiled. + The produced fonts will be larger, but rendering performance will + be improved with HarfBuzz and other text layout engines. + """ + ), + default=False, + parse=Option.parse_optional_bool, + validate=Option.validate_optional_bool, +) diff --git a/Lib/fontTools/cu2qu/cu2qu.py b/Lib/fontTools/cu2qu/cu2qu.py index e620b48a55..c0d4cf947b 100644 --- a/Lib/fontTools/cu2qu/cu2qu.py +++ b/Lib/fontTools/cu2qu/cu2qu.py @@ -17,13 +17,10 @@ try: import cython - - COMPILED = cython.compiled except (AttributeError, ImportError): # if cython not installed, use mock module with no-op decorators and types from fontTools.misc import cython - - COMPILED = False +COMPILED = cython.compiled import math diff --git a/Lib/fontTools/designspaceLib/__init__.py b/Lib/fontTools/designspaceLib/__init__.py index 342f1decd5..661f3405da 100644 --- a/Lib/fontTools/designspaceLib/__init__.py +++ b/Lib/fontTools/designspaceLib/__init__.py @@ -1,3 +1,9 @@ +""" + designSpaceDocument + + - Read and write designspace files +""" + from __future__ import annotations import collections @@ -15,11 +21,6 @@ from fontTools.misc.loggingTools import LogMixin from fontTools.misc.textTools import tobytes, tostr -""" - designSpaceDocument - - - read and write designspace files -""" __all__ = [ "AxisDescriptor", @@ -1595,7 +1596,7 @@ def _addAxis(self, axisObject): mapElement.attrib["input"] = self.intOrFloat(inputValue) mapElement.attrib["output"] = self.intOrFloat(outputValue) axisElement.append(mapElement) - if axisObject.axisOrdering or axisObject.axisLabels: + if axisObject.axisOrdering is not None or axisObject.axisLabels: labelsElement = ET.Element("labels") if axisObject.axisOrdering is not None: labelsElement.attrib["ordering"] = str(axisObject.axisOrdering) 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 17c6cc3fbe..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() @@ -595,8 +664,8 @@ def asFea(self, indent=""): class AlternateSubstStatement(Statement): """A ``sub ... from ...`` statement. - ``prefix``, ``glyph``, ``suffix`` and ``replacement`` should be lists of - `glyph-containing objects`_. ``glyph`` should be a `one element list`.""" + ``glyph`` and ``replacement`` should be `glyph-containing objects`_. + ``prefix`` and ``suffix`` should be lists of `glyph-containing objects`_.""" def __init__(self, prefix, glyph, suffix, replacement, location=None): Statement.__init__(self, location) @@ -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 @@ -1828,15 +1902,16 @@ class BaseAxis(Statement): """An axis definition, being either a ``VertAxis.BaseTagList/BaseScriptList`` pair or a ``HorizAxis.BaseTagList/BaseScriptList`` pair.""" - def __init__(self, bases, scripts, vertical, location=None): + def __init__(self, bases, scripts, vertical, minmax=None, location=None): Statement.__init__(self, location) self.bases = bases #: A list of baseline tag names as strings self.scripts = scripts #: A list of script record tuplets (script tag, default baseline tag, base coordinate) self.vertical = vertical #: Boolean; VertAxis if True, HorizAxis if False + self.minmax = [] #: A set of minmax record def build(self, builder): """Calls the builder object's ``set_base_axis`` callback.""" - builder.set_base_axis(self.bases, self.scripts, self.vertical) + builder.set_base_axis(self.bases, self.scripts, self.vertical, self.minmax) def asFea(self, indent=""): direction = "Vert" if self.vertical else "Horiz" @@ -1844,9 +1919,13 @@ def asFea(self, indent=""): "{} {} {}".format(a[0], a[1], " ".join(map(str, a[2]))) for a in self.scripts ] + minmaxes = [ + "\n{}Axis.MinMax {} {} {}, {};".format(direction, a[0], a[1], a[2], a[3]) + for a in self.minmax + ] return "{}Axis.BaseTagList {};\n{}{}Axis.BaseScriptList {};".format( direction, " ".join(self.bases), indent, direction, ", ".join(scripts) - ) + ) + "\n".join(minmaxes) class OS2Field(Statement): @@ -2098,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 a91381ddc1..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 @@ -341,6 +345,7 @@ def build_head(self): table = self.font["head"] = newTable("head") table.decompile(b"\0" * 54, self.font) table.tableVersion = 1.0 + table.magicNumber = 0x5F0F3CF5 table.created = table.modified = 3406620153 # 2011-12-13 11:22:33 table.fontRevision = self.fontRevision_ @@ -727,10 +732,16 @@ def buildBASE(self): result.table = base return result + def buildBASECoord(self, c): + coord = otTables.BaseCoord() + coord.Format = 1 + coord.Coordinate = c + return coord + def buildBASEAxis(self, axis): if not axis: return - bases, scripts = axis + bases, scripts, minmax = axis axis = otTables.Axis() axis.BaseTagList = otTables.BaseTagList() axis.BaseTagList.BaselineTag = bases @@ -739,19 +750,35 @@ def buildBASEAxis(self, axis): axis.BaseScriptList.BaseScriptRecord = [] axis.BaseScriptList.BaseScriptCount = len(scripts) for script in sorted(scripts): + minmax_for_script = [ + record[1:] for record in minmax if record[0] == script[0] + ] record = otTables.BaseScriptRecord() record.BaseScriptTag = script[0] record.BaseScript = otTables.BaseScript() - record.BaseScript.BaseLangSysCount = 0 record.BaseScript.BaseValues = otTables.BaseValues() record.BaseScript.BaseValues.DefaultIndex = bases.index(script[1]) record.BaseScript.BaseValues.BaseCoord = [] record.BaseScript.BaseValues.BaseCoordCount = len(script[2]) + record.BaseScript.BaseLangSysRecord = [] + for c in script[2]: - coord = otTables.BaseCoord() - coord.Format = 1 - coord.Coordinate = c - record.BaseScript.BaseValues.BaseCoord.append(coord) + record.BaseScript.BaseValues.BaseCoord.append(self.buildBASECoord(c)) + for language, min_coord, max_coord in minmax_for_script: + minmax_record = otTables.MinMax() + minmax_record.MinCoord = self.buildBASECoord(min_coord) + minmax_record.MaxCoord = self.buildBASECoord(max_coord) + minmax_record.FeatMinMaxCount = 0 + if language == "dflt": + record.BaseScript.DefaultMinMax = minmax_record + else: + lang_record = otTables.BaseLangSysRecord() + lang_record.BaseLangSysTag = language + lang_record.MinMax = minmax_record + record.BaseScript.BaseLangSysRecord.append(lang_record) + record.BaseScript.BaseLangSysCount = len( + record.BaseScript.BaseLangSysRecord + ) axis.BaseScriptList.BaseScriptRecord.append(record) return axis @@ -1031,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 @@ -1048,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 @@ -1063,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 @@ -1071,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 @@ -1106,7 +1143,13 @@ def set_language(self, location, language, include_default, required): if (language == "dflt" or include_default) and lookups: self.features_[key] = lookups[:] else: - self.features_[key] = [] + # if we aren't including default we need to manually remove the + # default lookups, which were added to all declared langsystems + # as they were encountered (we don't remove all lookups because + # we want to allow duplicate script/lang statements; + # see https://github.com/fonttools/fonttools/issues/3748 + cur_lookups = self.features_.get(key, []) + self.features_[key] = [x for x in cur_lookups if x not in lookups] self.language_systems = frozenset([(self.script_, language)]) if required: @@ -1229,11 +1272,11 @@ def add_to_cv_num_named_params(self, tag): def add_cv_character(self, character, tag): self.cv_characters_[tag].append(character) - def set_base_axis(self, bases, scripts, vertical): + def set_base_axis(self, bases, scripts, vertical, minmax=[]): if vertical: - self.base_vert_axis_ = (bases, scripts) + self.base_vert_axis_ = (bases, scripts, minmax) else: - self.base_horiz_axis_ = (bases, scripts) + self.base_horiz_axis_ = (bases, scripts, minmax) def set_size_parameters( self, location, DesignSize, SubfamilyID, RangeStart, RangeEnd @@ -1286,10 +1329,7 @@ def add_multiple_subst( self, location, prefix, glyph, suffix, replacements, forceChain=False ): if prefix or suffix or forceChain: - chain = self.get_lookup_(location, ChainContextSubstBuilder) - sub = self.get_chained_lookup_(location, MultipleSubstBuilder) - sub.mapping[glyph] = replacements - chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub])) + self.add_multi_subst_chained_(location, prefix, glyph, suffix, replacements) return lookup = self.get_lookup_(location, MultipleSubstBuilder) if glyph in lookup.mapping: @@ -1331,9 +1371,10 @@ def add_ligature_subst( self, location, prefix, glyphs, suffix, replacement, forceChain ): if prefix or suffix or forceChain: - chain = self.get_lookup_(location, ChainContextSubstBuilder) - lookup = self.get_chained_lookup_(location, LigatureSubstBuilder) - chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [lookup])) + self.add_ligature_subst_chained_( + location, prefix, glyphs, suffix, replacement + ) + return else: lookup = self.get_lookup_(location, LigatureSubstBuilder) @@ -1369,7 +1410,7 @@ def add_single_subst_chained_(self, location, prefix, suffix, mapping): # https://github.com/fonttools/fonttools/issues/512 # https://github.com/fonttools/fonttools/issues/2150 chain = self.get_lookup_(location, ChainContextSubstBuilder) - sub = chain.find_chainable_single_subst(mapping) + sub = chain.find_chainable_subst(mapping, SingleSubstBuilder) if sub is None: sub = self.get_chained_lookup_(location, SingleSubstBuilder) sub.mapping.update(mapping) @@ -1377,6 +1418,37 @@ def add_single_subst_chained_(self, location, prefix, suffix, mapping): ChainContextualRule(prefix, [list(mapping.keys())], suffix, [sub]) ) + def add_multi_subst_chained_(self, location, prefix, glyph, suffix, replacements): + if not all(prefix) or not all(suffix): + raise FeatureLibError( + "Empty glyph class in contextual substitution", location + ) + # https://github.com/fonttools/fonttools/issues/3551 + chain = self.get_lookup_(location, ChainContextSubstBuilder) + sub = chain.find_chainable_subst({glyph: replacements}, MultipleSubstBuilder) + if sub is None: + sub = self.get_chained_lookup_(location, MultipleSubstBuilder) + sub.mapping[glyph] = replacements + chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub])) + + def add_ligature_subst_chained_( + self, location, prefix, glyphs, suffix, replacement + ): + # https://github.com/fonttools/fonttools/issues/3701 + if not all(prefix) or not all(suffix): + raise FeatureLibError( + "Empty glyph class in contextual substitution", location + ) + chain = self.get_lookup_(location, ChainContextSubstBuilder) + sub = chain.find_chainable_ligature_subst(glyphs, replacement) + if sub is None: + sub = self.get_chained_lookup_(location, LigatureSubstBuilder) + + for g in itertools.product(*glyphs): + sub.ligatures[g] = replacement + + chain.rules.append(ChainContextualRule(prefix, glyphs, suffix, [sub])) + # GSUB 8 def add_reverse_chain_single_subst(self, location, old_prefix, old_suffix, mapping): if not mapping: @@ -1413,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: @@ -1648,38 +1722,31 @@ def makeVariablePos(self, location, varscalar): return default, device + def makeAnchorPos(self, varscalar, deviceTable, location): + device = None + if not isinstance(varscalar, VariableScalar): + if deviceTable is not None: + device = otl.buildDevice(dict(deviceTable)) + return varscalar, device + default, device = self.makeVariablePos(location, varscalar) + if device is not None and deviceTable is not None: + raise FeatureLibError( + "Can't define a device coordinate and variable scalar", location + ) + return default, device + def makeOpenTypeAnchor(self, location, anchor): """ast.Anchor --> otTables.Anchor""" if anchor is None: return None - variable = False deviceX, deviceY = None, None if anchor.xDeviceTable is not None: deviceX = otl.buildDevice(dict(anchor.xDeviceTable)) if anchor.yDeviceTable is not None: deviceY = otl.buildDevice(dict(anchor.yDeviceTable)) - for dim in ("x", "y"): - varscalar = getattr(anchor, dim) - if not isinstance(varscalar, VariableScalar): - continue - if getattr(anchor, dim + "DeviceTable") is not None: - raise FeatureLibError( - "Can't define a device coordinate and variable scalar", location - ) - default, device = self.makeVariablePos(location, varscalar) - setattr(anchor, dim, default) - if device is not None: - if dim == "x": - deviceX = device - else: - deviceY = device - variable = True - - otlanchor = otl.buildAnchor( - anchor.x, anchor.y, anchor.contourpoint, deviceX, deviceY - ) - if variable: - otlanchor.Format = 3 + x, deviceX = self.makeAnchorPos(anchor.x, anchor.xDeviceTable, location) + y, deviceY = self.makeAnchorPos(anchor.y, anchor.yDeviceTable, location) + otlanchor = otl.buildAnchor(x, y, anchor.contourpoint, deviceX, deviceY) return otlanchor _VALUEREC_ATTRS = { diff --git a/Lib/fontTools/feaLib/error.py b/Lib/fontTools/feaLib/error.py index a2c5f9dbb9..c5fed49348 100644 --- a/Lib/fontTools/feaLib/error.py +++ b/Lib/fontTools/feaLib/error.py @@ -1,5 +1,5 @@ class FeatureLibError(Exception): - def __init__(self, message, location): + def __init__(self, message, location=None): Exception.__init__(self, message) self.location = location diff --git a/Lib/fontTools/feaLib/lexer.py b/Lib/fontTools/feaLib/lexer.py index 5867f70b38..4b6499d06f 100644 --- a/Lib/fontTools/feaLib/lexer.py +++ b/Lib/fontTools/feaLib/lexer.py @@ -269,7 +269,7 @@ def make_lexer_(file_or_path): fileobj, closing = file_or_path, False else: filename, closing = file_or_path, True - fileobj = open(filename, "r", encoding="utf-8") + fileobj = open(filename, "r", encoding="utf-8-sig") data = fileobj.read() filename = getattr(fileobj, "name", None) if closing: diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 8cbe79592b..451dd62411 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -1286,6 +1286,19 @@ def unescape_byte_(match, encoding): n = match.group(0)[1:] return bytechr(int(n, 16)).decode(encoding) + def find_previous(self, statements, class_): + for previous in reversed(statements): + if isinstance(previous, self.ast.Comment): + continue + elif isinstance(previous, class_): + return previous + else: + # If we find something that doesn't match what we're looking + # for, and isn't a comment, fail + return None + # Out of statements to look at + return None + def parse_table_BASE_(self, table): statements = table.statements while self.next_token_ != "}" or self.cur_comments_: @@ -1306,6 +1319,19 @@ def parse_table_BASE_(self, table): location=self.cur_token_location_, ) ) + elif self.is_cur_keyword_("HorizAxis.MinMax"): + base_script_list = self.find_previous(statements, ast.BaseAxis) + if base_script_list is None: + raise FeatureLibError( + "MinMax must be preceded by BaseScriptList", + self.cur_token_location_, + ) + if base_script_list.vertical: + raise FeatureLibError( + "HorizAxis.MinMax must be preceded by HorizAxis statements", + self.cur_token_location_, + ) + base_script_list.minmax.append(self.parse_base_minmax_()) elif self.is_cur_keyword_("VertAxis.BaseTagList"): vert_bases = self.parse_base_tag_list_() elif self.is_cur_keyword_("VertAxis.BaseScriptList"): @@ -1318,6 +1344,19 @@ def parse_table_BASE_(self, table): location=self.cur_token_location_, ) ) + elif self.is_cur_keyword_("VertAxis.MinMax"): + base_script_list = self.find_previous(statements, ast.BaseAxis) + if base_script_list is None: + raise FeatureLibError( + "MinMax must be preceded by BaseScriptList", + self.cur_token_location_, + ) + if not base_script_list.vertical: + raise FeatureLibError( + "VertAxis.MinMax must be preceded by VertAxis statements", + self.cur_token_location_, + ) + base_script_list.minmax.append(self.parse_base_minmax_()) elif self.cur_token_ == ";": continue @@ -1574,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)) @@ -1587,6 +1626,25 @@ def parse_base_script_record_(self, count): coords = [self.expect_number_() for i in range(count)] return script_tag, base_tag, coords + def parse_base_minmax_(self): + script_tag = self.expect_script_tag_() + language = self.expect_language_tag_() + min_coord = self.expect_number_() + self.advance_lexer_() + if not (self.cur_token_type_ is Lexer.SYMBOL and self.cur_token_ == ","): + raise FeatureLibError( + "Expected a comma between min and max coordinates", + self.cur_token_location_, + ) + max_coord = self.expect_number_() + if self.next_token_ == ",": # feature tag... + raise FeatureLibError( + "Feature tags are not yet supported in BASE table", + self.cur_token_location_, + ) + + return script_tag, language, min_coord, max_coord + def parse_device_(self): result = None self.expect_symbol_("<") @@ -2004,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 16b7ee167d..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. @@ -918,7 +924,15 @@ def setupStat(self, axes, locations=None, elidedFallbackName=2): """ from .otlLib.builder import buildStatTable - buildStatTable(self.font, axes, locations, elidedFallbackName) + assert "name" in self.font, "name must to be set up first" + + buildStatTable( + self.font, + axes, + locations, + elidedFallbackName, + macNames=any(nr.platformID == 1 for nr in self.font["name"].names), + ) def buildCmapSubTable(cmapping, format, platformID, platEncID): @@ -938,6 +952,15 @@ def addFvar(font, axes, instances): fvar = newTable("fvar") nameTable = font["name"] + # if there are not currently any mac names don't add them here, that's inconsistent + # https://github.com/fonttools/fonttools/issues/683 + macNames = any(nr.platformID == 1 for nr in getattr(nameTable, "names", ())) + + # we have all the best ways to express mac names + platforms = ((3, 1, 0x409),) + if macNames: + platforms = ((1, 0, 0),) + platforms + for axis_def in axes: axis = Axis() @@ -963,7 +986,7 @@ def addFvar(font, axes, instances): if isinstance(name, str): name = dict(en=name) - axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font) + axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font, mac=macNames) fvar.axes.append(axis) for instance in instances: @@ -980,9 +1003,11 @@ def addFvar(font, axes, instances): name = dict(en=name) inst = NamedInstance() - inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font) + inst.subfamilyNameID = nameTable.addMultilingualName( + name, ttFont=font, mac=macNames + ) if psname is not None: - inst.postscriptNameID = nameTable.addName(psname) + inst.postscriptNameID = nameTable.addName(psname, platforms=platforms) inst.coordinates = coordinates fvar.instances.append(inst) diff --git a/Lib/fontTools/merge/__init__.py b/Lib/fontTools/merge/__init__.py index 7653e4a079..3f5875c586 100644 --- a/Lib/fontTools/merge/__init__.py +++ b/Lib/fontTools/merge/__init__.py @@ -27,24 +27,24 @@ class Merger(object): This class merges multiple files into a single OpenType font, taking into account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and - cross-font metrics (e.g. ``hhea.ascent`` is set to the maximum value across - all the fonts). + cross-font metrics (for example ``hhea.ascent`` is set to the maximum value + across all the fonts). If multiple glyphs map to the same Unicode value, and the glyphs are considered sufficiently different (that is, they differ in any of paths, widths, or height), then subsequent glyphs are renamed and a lookup in the ``locl`` feature will be created to disambiguate them. For example, if the arguments are an Arabic font and a Latin font and both contain a set of parentheses, - the Latin glyphs will be renamed to ``parenleft#1`` and ``parenright#1``, + the Latin glyphs will be renamed to ``parenleft.1`` and ``parenright.1``, and a lookup will be inserted into the to ``locl`` feature (creating it if necessary) under the ``latn`` script to substitute ``parenleft`` with - ``parenleft#1`` etc. + ``parenleft.1`` etc. Restrictions: - All fonts must have the same units per em. - If duplicate glyph disambiguation takes place as described above then the - fonts must have a ``GSUB`` table. + fonts must have a ``GSUB`` table. Attributes: options: Currently unused. diff --git a/Lib/fontTools/merge/cmap.py b/Lib/fontTools/merge/cmap.py index 3209a5d7b8..7cc4a4ead1 100644 --- a/Lib/fontTools/merge/cmap.py +++ b/Lib/fontTools/merge/cmap.py @@ -54,6 +54,28 @@ def _glyphsAreSame( return True +def computeMegaUvs(merger, uvsTables): + """Returns merged UVS subtable (cmap format=14).""" + uvsDict = {} + cmap = merger.cmap + for table in uvsTables: + for variationSelector, uvsMapping in table.uvsDict.items(): + if variationSelector not in uvsDict: + uvsDict[variationSelector] = {} + for unicodeValue, glyphName in uvsMapping: + if cmap.get(unicodeValue) == glyphName: + # this is a default variation + glyphName = None + # prefer previous glyph id if both fonts defined UVS + if unicodeValue not in uvsDict[variationSelector]: + uvsDict[variationSelector][unicodeValue] = glyphName + + for variationSelector in uvsDict: + uvsDict[variationSelector] = [*uvsDict[variationSelector].items()] + + return uvsDict + + # Valid (format, platformID, platEncID) triplets for cmap subtables containing # Unicode BMP-only and Unicode Full Repertoire semantics. # Cf. OpenType spec for "Platform specific encodings": @@ -61,24 +83,29 @@ def _glyphsAreSame( class _CmapUnicodePlatEncodings: BMP = {(4, 3, 1), (4, 0, 3), (4, 0, 4), (4, 0, 6)} FullRepertoire = {(12, 3, 10), (12, 0, 4), (12, 0, 6)} + UVS = {(14, 0, 5)} def computeMegaCmap(merger, cmapTables): - """Sets merger.cmap and merger.glyphOrder.""" + """Sets merger.cmap and merger.uvsDict.""" # TODO Handle format=14. # Only merge format 4 and 12 Unicode subtables, ignores all other subtables # If there is a format 12 table for a font, ignore the format 4 table of it chosenCmapTables = [] + chosenUvsTables = [] for fontIdx, table in enumerate(cmapTables): format4 = None format12 = None + format14 = None for subtable in table.tables: properties = (subtable.format, subtable.platformID, subtable.platEncID) if properties in _CmapUnicodePlatEncodings.BMP: format4 = subtable elif properties in _CmapUnicodePlatEncodings.FullRepertoire: format12 = subtable + elif properties in _CmapUnicodePlatEncodings.UVS: + format14 = subtable else: log.warning( "Dropped cmap subtable from font '%s':\t" @@ -93,6 +120,9 @@ def computeMegaCmap(merger, cmapTables): elif format4 is not None: chosenCmapTables.append((format4, fontIdx)) + if format14 is not None: + chosenUvsTables.append(format14) + # Build the unicode mapping merger.cmap = cmap = {} fontIndexForGlyph = {} @@ -127,6 +157,8 @@ def computeMegaCmap(merger, cmapTables): "Dropped mapping from codepoint %#06X to glyphId '%s'", uni, gid ) + merger.uvsDict = computeMegaUvs(merger, chosenUvsTables) + def renameCFFCharStrings(merger, glyphOrder, cffTable): """Rename topDictIndex charStrings based on glyphOrder.""" diff --git a/Lib/fontTools/merge/tables.py b/Lib/fontTools/merge/tables.py index 208a5099ff..a46977f0b4 100644 --- a/Lib/fontTools/merge/tables.py +++ b/Lib/fontTools/merge/tables.py @@ -312,7 +312,6 @@ def merge(self, m, tables): @add_method(ttLib.getTableClass("cmap")) def merge(self, m, tables): - # TODO Handle format=14. if not hasattr(m, "cmap"): computeMegaCmap(m, tables) cmap = m.cmap @@ -336,6 +335,18 @@ def merge(self, m, tables): cmapTable.cmap = cmapBmpOnly # ordered by platform then encoding self.tables.insert(0, cmapTable) + + uvsDict = m.uvsDict + if uvsDict: + # format-14 + uvsTable = module.cmap_classes[14](14) + uvsTable.platformID = 0 + uvsTable.platEncID = 5 + uvsTable.language = 0 + uvsTable.cmap = {} + uvsTable.uvsDict = uvsDict + # ordered by platform then encoding + self.tables.insert(0, uvsTable) self.tableVersion = 0 self.numSubTables = len(self.tables) return self diff --git a/Lib/fontTools/misc/bezierTools.py b/Lib/fontTools/misc/bezierTools.py index 5411ff99fd..2021f24437 100644 --- a/Lib/fontTools/misc/bezierTools.py +++ b/Lib/fontTools/misc/bezierTools.py @@ -9,13 +9,10 @@ try: import cython - - COMPILED = cython.compiled except (AttributeError, ImportError): # if cython not installed, use mock module with no-op decorators and types from fontTools.misc import cython - - COMPILED = False +COMPILED = cython.compiled EPSILON = 1e-9 @@ -634,7 +631,14 @@ def splitCubicAtT(pt1, pt2, pt3, pt4, *ts): ((77.3438, 56.25), (85.9375, 43.75), (93.75, 25), (100, 0)) """ a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4) - return _splitCubicAtT(a, b, c, d, *ts) + split = _splitCubicAtT(a, b, c, d, *ts) + + # the split impl can introduce floating point errors; we know the first + # segment should always start at pt1 and the last segment should end at pt4, + # so we set those values directly before returning. + split[0] = (pt1, *split[0][1:]) + split[-1] = (*split[-1][:-1], pt4) + return split @cython.locals( 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/misc/symfont.py b/Lib/fontTools/misc/symfont.py index 3a8819c774..4dea418408 100644 --- a/Lib/fontTools/misc/symfont.py +++ b/Lib/fontTools/misc/symfont.py @@ -121,13 +121,10 @@ def printGreenPen(penName, funcs, file=sys.stdout, docstring=None): """from fontTools.pens.basePen import BasePen, OpenContourError try: import cython - - COMPILED = cython.compiled except (AttributeError, ImportError): # if cython not installed, use mock module with no-op decorators and types from fontTools.misc import cython - - COMPILED = False +COMPILED = cython.compiled __all__ = ["%s"] diff --git a/Lib/fontTools/misc/testTools.py b/Lib/fontTools/misc/testTools.py index be6116132d..e2eaf38332 100644 --- a/Lib/fontTools/misc/testTools.py +++ b/Lib/fontTools/misc/testTools.py @@ -38,7 +38,7 @@ def parseXML(xmlSnippet): % type(xmlSnippet).__name__ ) xml += b"" - reader.parser.Parse(xml, 0) + reader.parser.Parse(xml, 1) return reader.root[2] @@ -46,7 +46,8 @@ def parseXmlInto(font, parseInto, xmlSnippet): parsed_xml = [e for e in parseXML(xmlSnippet.strip()) if not isinstance(e, str)] for name, attrs, content in parsed_xml: parseInto.fromXML(name, attrs, content, font) - parseInto.populateDefaults() + if hasattr(parseInto, "populateDefaults"): + parseInto.populateDefaults() return parseInto @@ -58,6 +59,9 @@ def __init__(self, glyphs): self.tables = {} self.cfg = Config() + def __contains__(self, tag): + return tag in self.tables + def __getitem__(self, tag): return self.tables[tag] diff --git a/Lib/fontTools/misc/transform.py b/Lib/fontTools/misc/transform.py index 9025b79ec1..aeacc30fcb 100644 --- a/Lib/fontTools/misc/transform.py +++ b/Lib/fontTools/misc/transform.py @@ -52,6 +52,8 @@ >>> """ +from __future__ import annotations + import math from typing import NamedTuple from dataclasses import dataclass @@ -65,7 +67,7 @@ _MINUS_ONE_EPSILON = -1 + _EPSILON -def _normSinCos(v): +def _normSinCos(v: float) -> float: if abs(v) < _EPSILON: v = 0 elif v > _ONE_EPSILON: @@ -214,7 +216,7 @@ def transformVectors(self, vectors): xx, xy, yx, yy = self[:4] return [(xx * dx + yx * dy, xy * dx + yy * dy) for dx, dy in vectors] - def translate(self, x=0, y=0): + def translate(self, x: float = 0, y: float = 0): """Return a new transformation, translated (offset) by x, y. :Example: @@ -225,7 +227,7 @@ def translate(self, x=0, y=0): """ return self.transform((1, 0, 0, 1, x, y)) - def scale(self, x=1, y=None): + def scale(self, x: float = 1, y: float | None = None): """Return a new transformation, scaled by x, y. The 'y' argument may be None, which implies to use the x value for y as well. @@ -241,7 +243,7 @@ def scale(self, x=1, y=None): y = x return self.transform((x, 0, 0, y, 0, 0)) - def rotate(self, angle): + def rotate(self, angle: float): """Return a new transformation, rotated by 'angle' (radians). :Example: @@ -251,13 +253,11 @@ def rotate(self, angle): >>> """ - import math - c = _normSinCos(math.cos(angle)) s = _normSinCos(math.sin(angle)) return self.transform((c, s, -s, c, 0, 0)) - def skew(self, x=0, y=0): + def skew(self, x: float = 0, y: float = 0): """Return a new transformation, skewed by x and y. :Example: @@ -267,8 +267,6 @@ def skew(self, x=0, y=0): >>> """ - import math - return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0)) def transform(self, other): @@ -336,7 +334,7 @@ def inverse(self): dx, dy = -xx * dx - yx * dy, -xy * dx - yy * dy return self.__class__(xx, xy, yx, yy, dx, dy) - def toPS(self): + def toPS(self) -> str: """Return a PostScript representation :Example: @@ -352,7 +350,7 @@ def toDecomposed(self) -> "DecomposedTransform": """Decompose into a DecomposedTransform.""" return DecomposedTransform.fromTransform(self) - def __bool__(self): + def __bool__(self) -> bool: """Returns True if transform is not identity, False otherwise. :Example: @@ -374,14 +372,14 @@ def __bool__(self): """ return self != Identity - def __repr__(self): + def __repr__(self) -> str: return "<%s [%g %g %g %g %g %g]>" % ((self.__class__.__name__,) + self) Identity = Transform() -def Offset(x=0, y=0): +def Offset(x: float = 0, y: float = 0) -> Transform: """Return the identity transformation offset by x, y. :Example: @@ -392,7 +390,7 @@ def Offset(x=0, y=0): return Transform(1, 0, 0, 1, x, y) -def Scale(x, y=None): +def Scale(x: float, y: float | None = None) -> Transform: """Return the identity transformation scaled by x, y. The 'y' argument may be None, which implies to use the x value for y as well. @@ -437,8 +435,20 @@ def __bool__(self): @classmethod def fromTransform(self, transform): + """Return a DecomposedTransform() equivalent of this transformation. + The returned solution always has skewY = 0, and angle in the (-180, 180]. + + :Example: + >>> DecomposedTransform.fromTransform(Transform(3, 0, 0, 2, 0, 0)) + DecomposedTransform(translateX=0, translateY=0, rotation=0.0, scaleX=3.0, scaleY=2.0, skewX=0.0, skewY=0.0, tCenterX=0, tCenterY=0) + >>> DecomposedTransform.fromTransform(Transform(0, 0, 0, 1, 0, 0)) + DecomposedTransform(translateX=0, translateY=0, rotation=0.0, scaleX=0.0, scaleY=1.0, skewX=0.0, skewY=0.0, tCenterX=0, tCenterY=0) + >>> DecomposedTransform.fromTransform(Transform(0, 0, 1, 1, 0, 0)) + DecomposedTransform(translateX=0, translateY=0, rotation=-45.0, scaleX=0.0, scaleY=1.4142135623730951, skewX=0.0, skewY=0.0, tCenterX=0, tCenterY=0) + """ # Adapted from an answer on # https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix + a, b, c, d, x, y = transform sx = math.copysign(1, a) @@ -450,21 +460,20 @@ def fromTransform(self, transform): rotation = 0 scaleX = scaleY = 0 - skewX = skewY = 0 + skewX = 0 # Apply the QR-like decomposition. if a != 0 or b != 0: r = math.sqrt(a * a + b * b) rotation = math.acos(a / r) if b >= 0 else -math.acos(a / r) scaleX, scaleY = (r, delta / r) - skewX, skewY = (math.atan((a * c + b * d) / (r * r)), 0) + skewX = math.atan((a * c + b * d) / (r * r)) elif c != 0 or d != 0: s = math.sqrt(c * c + d * d) rotation = math.pi / 2 - ( math.acos(-c / s) if d >= 0 else -math.acos(c / s) ) scaleX, scaleY = (delta / s, s) - skewX, skewY = (0, math.atan((a * c + b * d) / (s * s))) else: # a = b = c = d = 0 pass @@ -476,12 +485,12 @@ def fromTransform(self, transform): scaleX * sx, scaleY, math.degrees(skewX) * sx, - math.degrees(skewY), + 0.0, 0, 0, ) - def toTransform(self): + def toTransform(self) -> Transform: """Return the Transform() equivalent of this transformation. :Example: diff --git a/Lib/fontTools/misc/visitor.py b/Lib/fontTools/misc/visitor.py index d289895467..6de432ef93 100644 --- a/Lib/fontTools/misc/visitor.py +++ b/Lib/fontTools/misc/visitor.py @@ -61,9 +61,10 @@ def _visitorsFor(celf, thing, _default={}): if _visitors is None: break - m = celf._visitors.get(typ, None) - if m is not None: - return m + for base in typ.mro(): + m = celf._visitors.get(base, None) + if m is not None: + return m return _default diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py index dbedf275e3..32d3a244e1 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: @@ -1377,7 +1375,7 @@ def main(args=None, font=None): for f in args.inputs: log.debug("Processing %s", f) - with open(f, "rt", encoding="utf-8") as f: + with open(f, "rt", encoding="utf-8-sig") as f: table = build(f, font, tableTag=args.tableTag) blob = table.compile(font) # Make sure it compiles decompiled = table.__class__() diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 70fd87ab57..064b2fce31 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from collections import namedtuple, OrderedDict -import os +import itertools +from typing import Dict, Union from fontTools.misc.fixedTools import fixedToFloat from fontTools.misc.roundTools import otRound from fontTools import ttLib @@ -9,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 @@ -72,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 `__) @@ -97,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 @@ -112,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) @@ -132,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 @@ -140,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): @@ -148,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): @@ -159,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} @@ -544,6 +570,10 @@ def buildFormat2Subtable(self, ruleset, classdefs, chaining=True): f"{classRuleAttr}Count", getattr(setForThisRule, f"{classRuleAttr}Count") + 1, ) + for i, classSet in enumerate(classSets): + if not getattr(classSet, classRuleAttr): + # class sets can be null so replace nop sets with None + classSets[i] = None setattr(st, self.ruleSetAttr_(format=2, chaining=chaining), classSets) setattr( st, self.ruleSetAttr_(format=2, chaining=chaining) + "Count", len(classSets) @@ -781,19 +811,35 @@ def getAlternateGlyphs(self): ) return result - def find_chainable_single_subst(self, mapping): - """Helper for add_single_subst_chained_()""" + def find_chainable_subst(self, mapping, builder_class): + """Helper for add_{single,multi}_subst_chained_()""" res = None for rule in self.rules[::-1]: if rule.is_subtable_break: return res for sub in rule.lookups: - if isinstance(sub, SingleSubstBuilder) and not any( + if isinstance(sub, builder_class) and not any( g in mapping and mapping[g] != sub.mapping[g] for g in sub.mapping ): res = sub return res + def find_chainable_ligature_subst(self, glyphs, replacement): + """Helper for add_ligature_subst_chained_()""" + res = None + for rule in self.rules[::-1]: + if rule.is_subtable_break: + return res + for sub in rule.lookups: + if not isinstance(sub, LigatureSubstBuilder): + continue + if all( + sub.ligatures.get(seq, replacement) == replacement + for seq in itertools.product(*glyphs) + ): + res = sub + return res + class LigatureSubstBuilder(LookupBuilder): """Builds a Ligature Substitution (GSUB4) lookup. @@ -928,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): @@ -964,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): @@ -984,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. @@ -1040,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): @@ -1060,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. @@ -1104,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): @@ -1124,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): @@ -1463,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( @@ -1879,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)] @@ -1933,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 @@ -1960,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 @@ -1988,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 @@ -2685,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 @@ -2879,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/maxContextCalc.py b/Lib/fontTools/otlLib/maxContextCalc.py index 03e7561b60..eee8d48f3c 100644 --- a/Lib/fontTools/otlLib/maxContextCalc.py +++ b/Lib/fontTools/otlLib/maxContextCalc.py @@ -92,5 +92,5 @@ def maxCtxContextualRule(maxCtx, st, chain): if not chain: return max(maxCtx, st.GlyphCount) elif chain == "Reverse": - return max(maxCtx, st.GlyphCount + st.LookAheadGlyphCount) + return max(maxCtx, 1 + st.LookAheadGlyphCount) return max(maxCtx, st.InputGlyphCount + st.LookAheadGlyphCount) diff --git a/Lib/fontTools/otlLib/optimize/gpos.py b/Lib/fontTools/otlLib/optimize/gpos.py index 01c2257cd7..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 @@ -53,12 +54,18 @@ def compact(font: TTFont, level: int) -> TTFont: # are not grouped together first; instead each subtable is treated # independently, so currently this step is: # Split existing subtables into more smaller subtables - gpos = font["GPOS"] + gpos = font.get("GPOS") + + # If the font does not contain a GPOS table, there is nothing to do. + if gpos is None: + return font + for lookup in gpos.table.LookupList.Lookup: if lookup.LookupType == 2: compact_lookup(font, level, lookup) elif lookup.LookupType == 9 and lookup.SubTable[0].ExtensionLookupType == 2: compact_ext_lookup(font, level, lookup) + return font @@ -186,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/freetypePen.py b/Lib/fontTools/pens/freetypePen.py index 870776bc7b..065da932a2 100644 --- a/Lib/fontTools/pens/freetypePen.py +++ b/Lib/fontTools/pens/freetypePen.py @@ -46,7 +46,7 @@ class FreeTypePen(BasePen): glyphSet: a dictionary of drawable glyph objects keyed by name used to resolve component references in composite glyphs. - :Examples: + Examples: If `numpy` and `matplotlib` is available, the following code will show the glyph image of `fi` in a new window:: @@ -178,7 +178,7 @@ def buffer( object of the resulted bitmap and ``size`` is a 2-tuple of its dimension. - :Notes: + Notes: The image size should always be given explicitly if you need to get a proper glyph image. When ``width`` and ``height`` are omitted, it forcifully fits to the bounding box and the side bearings get @@ -188,15 +188,15 @@ def buffer( maintained but RSB won’t. The difference between the two becomes more obvious when rotate or skew transformation is applied. - :Example: - .. code-block:: + Example: + .. code-block:: pycon + >>> >> pen = FreeTypePen(None) >> glyph.draw(pen) >> buf, size = pen.buffer(width=500, height=1000) >> type(buf), len(buf), size (, 500000, (500, 1000)) - """ transform = transform or Transform() if not hasattr(transform, "transformPoint"): @@ -269,7 +269,7 @@ def array( A ``numpy.ndarray`` object with a shape of ``(height, width)``. Each element takes a value in the range of ``[0.0, 1.0]``. - :Notes: + Notes: The image size should always be given explicitly if you need to get a proper glyph image. When ``width`` and ``height`` are omitted, it forcifully fits to the bounding box and the side bearings get @@ -279,15 +279,17 @@ def array( maintained but RSB won’t. The difference between the two becomes more obvious when rotate or skew transformation is applied. - :Example: - .. code-block:: + Example: + .. code-block:: pycon + >>> >> pen = FreeTypePen(None) >> glyph.draw(pen) >> arr = pen.array(width=500, height=1000) >> type(a), a.shape (, (1000, 500)) """ + import numpy as np buf, size = self.buffer( @@ -318,7 +320,7 @@ def show( rendering glyphs with negative sidebearings without clipping. evenOdd: Pass ``True`` for even-odd fill instead of non-zero. - :Notes: + Notes: The image size should always be given explicitly if you need to get a proper glyph image. When ``width`` and ``height`` are omitted, it forcifully fits to the bounding box and the side bearings get @@ -328,9 +330,10 @@ def show( maintained but RSB won’t. The difference between the two becomes more obvious when rotate or skew transformation is applied. - :Example: - .. code-block:: + Example: + .. code-block:: pycon + >>> >> pen = FreeTypePen(None) >> glyph.draw(pen) >> pen.show(width=500, height=1000) @@ -370,7 +373,7 @@ def image( A ``PIL.image`` object. The image is filled in black with alpha channel obtained from the rendered bitmap. - :Notes: + Notes: The image size should always be given explicitly if you need to get a proper glyph image. When ``width`` and ``height`` are omitted, it forcifully fits to the bounding box and the side bearings get @@ -380,9 +383,10 @@ def image( maintained but RSB won’t. The difference between the two becomes more obvious when rotate or skew transformation is applied. - :Example: - .. code-block:: + Example: + .. code-block:: pycon + >>> >> pen = FreeTypePen(None) >> glyph.draw(pen) >> img = pen.image(width=500, height=1000) diff --git a/Lib/fontTools/pens/momentsPen.py b/Lib/fontTools/pens/momentsPen.py index 2afb8fdbd1..77ead9fc31 100644 --- a/Lib/fontTools/pens/momentsPen.py +++ b/Lib/fontTools/pens/momentsPen.py @@ -2,13 +2,10 @@ try: import cython - - COMPILED = cython.compiled except (AttributeError, ImportError): # if cython not installed, use mock module with no-op decorators and types from fontTools.misc import cython - - COMPILED = False +COMPILED = cython.compiled __all__ = ["MomentsPen"] diff --git a/Lib/fontTools/pens/pointInsidePen.py b/Lib/fontTools/pens/pointInsidePen.py index e1fbbbcb1d..0c022d31bc 100644 --- a/Lib/fontTools/pens/pointInsidePen.py +++ b/Lib/fontTools/pens/pointInsidePen.py @@ -15,7 +15,8 @@ class PointInsidePen(BasePen): Instances of this class can be recycled, as long as the setTestPoint() method is used to set the new point to test. - Typical usage: + :Example: + .. code-block:: pen = PointInsidePen(glyphSet, (100, 200)) outline.draw(pen) 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/pens/recordingPen.py b/Lib/fontTools/pens/recordingPen.py index ba165e1951..b8a817ccf4 100644 --- a/Lib/fontTools/pens/recordingPen.py +++ b/Lib/fontTools/pens/recordingPen.py @@ -33,6 +33,7 @@ class RecordingPen(AbstractPen): pen.replay(otherPen). :Example: + .. code-block:: from fontTools.ttLib import TTFont from fontTools.pens.recordingPen import RecordingPen @@ -91,47 +92,48 @@ class DecomposingRecordingPen(DecomposingPen, RecordingPen): by thir name; other arguments are forwarded to the DecomposingPen's constructor:: - >>> class SimpleGlyph(object): - ... def draw(self, pen): - ... pen.moveTo((0, 0)) - ... pen.curveTo((1, 1), (2, 2), (3, 3)) - ... pen.closePath() - >>> class CompositeGlyph(object): - ... def draw(self, pen): - ... pen.addComponent('a', (1, 0, 0, 1, -1, 1)) - >>> class MissingComponent(object): - ... def draw(self, pen): - ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0)) - >>> class FlippedComponent(object): - ... def draw(self, pen): - ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0)) - >>> glyphSet = { - ... 'a': SimpleGlyph(), - ... 'b': CompositeGlyph(), - ... 'c': MissingComponent(), - ... 'd': FlippedComponent(), - ... } - >>> for name, glyph in sorted(glyphSet.items()): - ... pen = DecomposingRecordingPen(glyphSet) - ... try: - ... glyph.draw(pen) - ... except pen.MissingComponentError: - ... pass - ... print("{}: {}".format(name, pen.value)) - a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())] - b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())] - c: [] - d: [('moveTo', ((0, 0),)), ('curveTo', ((-1, 1), (-2, 2), (-3, 3))), ('closePath', ())] - >>> for name, glyph in sorted(glyphSet.items()): - ... pen = DecomposingRecordingPen( - ... glyphSet, skipMissingComponents=True, reverseFlipped=True, - ... ) - ... glyph.draw(pen) - ... print("{}: {}".format(name, pen.value)) - a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())] - b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())] - c: [] - d: [('moveTo', ((0, 0),)), ('lineTo', ((-3, 3),)), ('curveTo', ((-2, 2), (-1, 1), (0, 0))), ('closePath', ())] + >>> class SimpleGlyph(object): + ... def draw(self, pen): + ... pen.moveTo((0, 0)) + ... pen.curveTo((1, 1), (2, 2), (3, 3)) + ... pen.closePath() + >>> class CompositeGlyph(object): + ... def draw(self, pen): + ... pen.addComponent('a', (1, 0, 0, 1, -1, 1)) + >>> class MissingComponent(object): + ... def draw(self, pen): + ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0)) + >>> class FlippedComponent(object): + ... def draw(self, pen): + ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0)) + >>> glyphSet = { + ... 'a': SimpleGlyph(), + ... 'b': CompositeGlyph(), + ... 'c': MissingComponent(), + ... 'd': FlippedComponent(), + ... } + >>> for name, glyph in sorted(glyphSet.items()): + ... pen = DecomposingRecordingPen(glyphSet) + ... try: + ... glyph.draw(pen) + ... except pen.MissingComponentError: + ... pass + ... print("{}: {}".format(name, pen.value)) + a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())] + b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())] + c: [] + d: [('moveTo', ((0, 0),)), ('curveTo', ((-1, 1), (-2, 2), (-3, 3))), ('closePath', ())] + + >>> for name, glyph in sorted(glyphSet.items()): + ... pen = DecomposingRecordingPen( + ... glyphSet, skipMissingComponents=True, reverseFlipped=True, + ... ) + ... glyph.draw(pen) + ... print("{}: {}".format(name, pen.value)) + a: [('moveTo', ((0, 0),)), ('curveTo', ((1, 1), (2, 2), (3, 3))), ('closePath', ())] + b: [('moveTo', ((-1, 1),)), ('curveTo', ((0, 2), (1, 3), (2, 4))), ('closePath', ())] + c: [] + d: [('moveTo', ((0, 0),)), ('lineTo', ((-3, 3),)), ('curveTo', ((-2, 2), (-1, 1), (0, 0))), ('closePath', ())] """ # raises MissingComponentError(KeyError) if base glyph is not found in glyphSet @@ -145,6 +147,7 @@ class RecordingPointPen(AbstractPointPen): pointPen.replay(otherPointPen). :Example: + .. code-block:: from defcon import Font from fontTools.pens.recordingPen import RecordingPointPen @@ -211,81 +214,82 @@ class DecomposingRecordingPointPen(DecomposingPointPen, RecordingPointPen): keyed by thir name; other arguments are forwarded to the DecomposingPointPen's constructor:: - >>> from pprint import pprint - >>> class SimpleGlyph(object): - ... def drawPoints(self, pen): - ... pen.beginPath() - ... pen.addPoint((0, 0), "line") - ... pen.addPoint((1, 1)) - ... pen.addPoint((2, 2)) - ... pen.addPoint((3, 3), "curve") - ... pen.endPath() - >>> class CompositeGlyph(object): - ... def drawPoints(self, pen): - ... pen.addComponent('a', (1, 0, 0, 1, -1, 1)) - >>> class MissingComponent(object): - ... def drawPoints(self, pen): - ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0)) - >>> class FlippedComponent(object): - ... def drawPoints(self, pen): - ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0)) - >>> glyphSet = { - ... 'a': SimpleGlyph(), - ... 'b': CompositeGlyph(), - ... 'c': MissingComponent(), - ... 'd': FlippedComponent(), - ... } - >>> for name, glyph in sorted(glyphSet.items()): - ... pen = DecomposingRecordingPointPen(glyphSet) - ... try: - ... glyph.drawPoints(pen) - ... except pen.MissingComponentError: - ... pass - ... pprint({name: pen.value}) - {'a': [('beginPath', (), {}), - ('addPoint', ((0, 0), 'line', False, None), {}), - ('addPoint', ((1, 1), None, False, None), {}), - ('addPoint', ((2, 2), None, False, None), {}), - ('addPoint', ((3, 3), 'curve', False, None), {}), - ('endPath', (), {})]} - {'b': [('beginPath', (), {}), - ('addPoint', ((-1, 1), 'line', False, None), {}), - ('addPoint', ((0, 2), None, False, None), {}), - ('addPoint', ((1, 3), None, False, None), {}), - ('addPoint', ((2, 4), 'curve', False, None), {}), - ('endPath', (), {})]} - {'c': []} - {'d': [('beginPath', (), {}), - ('addPoint', ((0, 0), 'line', False, None), {}), - ('addPoint', ((-1, 1), None, False, None), {}), - ('addPoint', ((-2, 2), None, False, None), {}), - ('addPoint', ((-3, 3), 'curve', False, None), {}), - ('endPath', (), {})]} - >>> for name, glyph in sorted(glyphSet.items()): - ... pen = DecomposingRecordingPointPen( - ... glyphSet, skipMissingComponents=True, reverseFlipped=True, - ... ) - ... glyph.drawPoints(pen) - ... pprint({name: pen.value}) - {'a': [('beginPath', (), {}), - ('addPoint', ((0, 0), 'line', False, None), {}), - ('addPoint', ((1, 1), None, False, None), {}), - ('addPoint', ((2, 2), None, False, None), {}), - ('addPoint', ((3, 3), 'curve', False, None), {}), - ('endPath', (), {})]} - {'b': [('beginPath', (), {}), - ('addPoint', ((-1, 1), 'line', False, None), {}), - ('addPoint', ((0, 2), None, False, None), {}), - ('addPoint', ((1, 3), None, False, None), {}), - ('addPoint', ((2, 4), 'curve', False, None), {}), - ('endPath', (), {})]} - {'c': []} - {'d': [('beginPath', (), {}), - ('addPoint', ((0, 0), 'curve', False, None), {}), - ('addPoint', ((-3, 3), 'line', False, None), {}), - ('addPoint', ((-2, 2), None, False, None), {}), - ('addPoint', ((-1, 1), None, False, None), {}), - ('endPath', (), {})]} + >>> from pprint import pprint + >>> class SimpleGlyph(object): + ... def drawPoints(self, pen): + ... pen.beginPath() + ... pen.addPoint((0, 0), "line") + ... pen.addPoint((1, 1)) + ... pen.addPoint((2, 2)) + ... pen.addPoint((3, 3), "curve") + ... pen.endPath() + >>> class CompositeGlyph(object): + ... def drawPoints(self, pen): + ... pen.addComponent('a', (1, 0, 0, 1, -1, 1)) + >>> class MissingComponent(object): + ... def drawPoints(self, pen): + ... pen.addComponent('foobar', (1, 0, 0, 1, 0, 0)) + >>> class FlippedComponent(object): + ... def drawPoints(self, pen): + ... pen.addComponent('a', (-1, 0, 0, 1, 0, 0)) + >>> glyphSet = { + ... 'a': SimpleGlyph(), + ... 'b': CompositeGlyph(), + ... 'c': MissingComponent(), + ... 'd': FlippedComponent(), + ... } + >>> for name, glyph in sorted(glyphSet.items()): + ... pen = DecomposingRecordingPointPen(glyphSet) + ... try: + ... glyph.drawPoints(pen) + ... except pen.MissingComponentError: + ... pass + ... pprint({name: pen.value}) + {'a': [('beginPath', (), {}), + ('addPoint', ((0, 0), 'line', False, None), {}), + ('addPoint', ((1, 1), None, False, None), {}), + ('addPoint', ((2, 2), None, False, None), {}), + ('addPoint', ((3, 3), 'curve', False, None), {}), + ('endPath', (), {})]} + {'b': [('beginPath', (), {}), + ('addPoint', ((-1, 1), 'line', False, None), {}), + ('addPoint', ((0, 2), None, False, None), {}), + ('addPoint', ((1, 3), None, False, None), {}), + ('addPoint', ((2, 4), 'curve', False, None), {}), + ('endPath', (), {})]} + {'c': []} + {'d': [('beginPath', (), {}), + ('addPoint', ((0, 0), 'line', False, None), {}), + ('addPoint', ((-1, 1), None, False, None), {}), + ('addPoint', ((-2, 2), None, False, None), {}), + ('addPoint', ((-3, 3), 'curve', False, None), {}), + ('endPath', (), {})]} + + >>> for name, glyph in sorted(glyphSet.items()): + ... pen = DecomposingRecordingPointPen( + ... glyphSet, skipMissingComponents=True, reverseFlipped=True, + ... ) + ... glyph.drawPoints(pen) + ... pprint({name: pen.value}) + {'a': [('beginPath', (), {}), + ('addPoint', ((0, 0), 'line', False, None), {}), + ('addPoint', ((1, 1), None, False, None), {}), + ('addPoint', ((2, 2), None, False, None), {}), + ('addPoint', ((3, 3), 'curve', False, None), {}), + ('endPath', (), {})]} + {'b': [('beginPath', (), {}), + ('addPoint', ((-1, 1), 'line', False, None), {}), + ('addPoint', ((0, 2), None, False, None), {}), + ('addPoint', ((1, 3), None, False, None), {}), + ('addPoint', ((2, 4), 'curve', False, None), {}), + ('endPath', (), {})]} + {'c': []} + {'d': [('beginPath', (), {}), + ('addPoint', ((0, 0), 'curve', False, None), {}), + ('addPoint', ((-3, 3), 'line', False, None), {}), + ('addPoint', ((-2, 2), None, False, None), {}), + ('addPoint', ((-1, 1), None, False, None), {}), + ('endPath', (), {})]} """ # raises MissingComponentError(KeyError) if base glyph is not found in glyphSet diff --git a/Lib/fontTools/pens/statisticsPen.py b/Lib/fontTools/pens/statisticsPen.py index b91d93b6eb..874a3c5b8d 100644 --- a/Lib/fontTools/pens/statisticsPen.py +++ b/Lib/fontTools/pens/statisticsPen.py @@ -106,6 +106,7 @@ def __init__(self, glyphset=None): def _moveTo(self, pt): self._nodes.append(complex(*pt)) + self._startPoint = pt def _lineTo(self, pt): self._nodes.append(complex(*pt)) @@ -119,12 +120,16 @@ def _curveToOne(self, pt1, pt2, pt3): self._nodes.append(complex(*pt)) def _closePath(self): + p0 = self._getCurrentPoint() + if p0 != self._startPoint: + self._lineTo(self._startPoint) self._update() def _endPath(self): p0 = self._getCurrentPoint() if p0 != self._startPoint: raise OpenContourError("Glyph statistics not defined on open contours.") + self._update() def _update(self): nodes = self._nodes diff --git a/Lib/fontTools/pens/svgPathPen.py b/Lib/fontTools/pens/svgPathPen.py index 8231603f8a..29d128da36 100644 --- a/Lib/fontTools/pens/svgPathPen.py +++ b/Lib/fontTools/pens/svgPathPen.py @@ -2,34 +2,37 @@ from fontTools.pens.basePen import BasePen -def pointToString(pt, ntos): +def pointToString(pt, ntos=str): return " ".join(ntos(i) for i in pt) class SVGPathPen(BasePen): """Pen to draw SVG path d commands. - Example:: - >>> pen = SVGPathPen(None) - >>> pen.moveTo((0, 0)) - >>> pen.lineTo((1, 1)) - >>> pen.curveTo((2, 2), (3, 3), (4, 4)) - >>> pen.closePath() - >>> pen.getCommands() - 'M0 0 1 1C2 2 3 3 4 4Z' - Args: glyphSet: a dictionary of drawable glyph objects keyed by name used to resolve component references in composite glyphs. ntos: a callable that takes a number and returns a string, to customize how numbers are formatted (default: str). + :Example: + .. code-block:: + + >>> pen = SVGPathPen(None) + >>> pen.moveTo((0, 0)) + >>> pen.lineTo((1, 1)) + >>> pen.curveTo((2, 2), (3, 3), (4, 4)) + >>> pen.closePath() + >>> pen.getCommands() + 'M0 0 1 1C2 2 3 3 4 4Z' + Note: Fonts have a coordinate system where Y grows up, whereas in SVG, Y grows down. As such, rendering path data from this pen in SVG typically results in upside-down glyphs. You can fix this by wrapping the data from this pen in an SVG group element with transform, or wrap this pen in a transform pen. For example: + .. code-block:: python spen = svgPathPen.SVGPathPen(glyphset) pen= TransformPen(spen , (1, 0, 0, -1, 0, 0)) @@ -37,13 +40,7 @@ class SVGPathPen(BasePen): print(tpen.getCommands()) """ - def __init__( - self, - glyphSet, - ntos: Callable[[float], str] = ( - lambda x: ("%.2f" % x) if x != int(x) else str(int(x)) - ), - ): + def __init__(self, glyphSet, ntos: Callable[[float], str] = str): BasePen.__init__(self, glyphSet) self._commands = [] self._lastCommand = None diff --git a/Lib/fontTools/pens/transformPen.py b/Lib/fontTools/pens/transformPen.py index ff98dbddb0..3db6efdf2f 100644 --- a/Lib/fontTools/pens/transformPen.py +++ b/Lib/fontTools/pens/transformPen.py @@ -58,22 +58,27 @@ class TransformPointPen(FilterPointPen): """PointPen that transforms all coordinates using a Affine transformation, and passes them to another PointPen. - >>> from fontTools.pens.recordingPen import RecordingPointPen - >>> rec = RecordingPointPen() - >>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5)) - >>> v = iter(rec.value) - >>> pen.beginPath(identifier="contour-0") - >>> next(v) - ('beginPath', (), {'identifier': 'contour-0'}) - >>> pen.addPoint((100, 100), "line") - >>> next(v) - ('addPoint', ((190, 205), 'line', False, None), {}) - >>> pen.endPath() - >>> next(v) - ('endPath', (), {}) - >>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0") - >>> next(v) - ('addComponent', ('a', ), {'identifier': 'component-0'}) + For example:: + + >>> from fontTools.pens.recordingPen import RecordingPointPen + >>> rec = RecordingPointPen() + >>> pen = TransformPointPen(rec, (2, 0, 0, 2, -10, 5)) + >>> v = iter(rec.value) + >>> pen.beginPath(identifier="contour-0") + >>> next(v) + ('beginPath', (), {'identifier': 'contour-0'}) + + >>> pen.addPoint((100, 100), "line") + >>> next(v) + ('addPoint', ((190, 205), 'line', False, None), {}) + + >>> pen.endPath() + >>> next(v) + ('endPath', (), {}) + + >>> pen.addComponent("a", (1, 0, 0, 1, -10, 5), identifier="component-0") + >>> next(v) + ('addComponent', ('a', ), {'identifier': 'component-0'}) """ def __init__(self, outPointPen, transformation): diff --git a/Lib/fontTools/qu2cu/qu2cu.py b/Lib/fontTools/qu2cu/qu2cu.py index 97a665f63a..8fe3e18b08 100644 --- a/Lib/fontTools/qu2cu/qu2cu.py +++ b/Lib/fontTools/qu2cu/qu2cu.py @@ -18,13 +18,10 @@ try: import cython - - COMPILED = cython.compiled except (AttributeError, ImportError): # if cython not installed, use mock module with no-op decorators and types from fontTools.misc import cython - - COMPILED = False +COMPILED = cython.compiled from fontTools.misc.bezierTools import splitCubicAtTC from collections import namedtuple diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 4aa60ad842..71d5f5d80d 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 @@ -122,15 +123,16 @@ ^^^^^^^^^^^^^ For the other options listed below, to see the current value of the option, -pass a value of '?' to it, with or without a '='. +pass a value of '?' to it, with or without a '='. In some environments, +you might need to escape the question mark, like this: '--glyph-names\\?'. Examples:: $ pyftsubset --glyph-names? Current setting for 'glyph-names' is: False - $ ./pyftsubset --name-IDs=? + $ pyftsubset --name-IDs=? Current setting for 'name-IDs' is: [0, 1, 2, 3, 4, 5, 6] - $ ./pyftsubset --hinting? --no-hinting --hinting? + $ pyftsubset --hinting? --no-hinting --hinting? Current setting for 'hinting' is: True Current setting for 'hinting' is: False @@ -2869,10 +2871,21 @@ 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: - for cmap in table.uvsDict.values(): + for varSelector, cmap in table.uvsDict.items(): + if varSelector not in s.unicodes_requested: + continue glyphs = {g for u, g in cmap if u in s.unicodes_requested} if None in glyphs: glyphs.remove(None) @@ -2927,6 +2940,7 @@ def subset_glyphs(self, s): if g in s.glyphs_requested or u in s.unicodes_requested ] for v, l in t.uvsDict.items() + if v in s.unicodes_requested } t.uvsDict = {v: l for v, l in t.uvsDict.items() if l} elif t.isUnicode(): @@ -3187,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) @@ -3742,7 +3757,7 @@ def main(args=None): text += g[7:] continue if g.startswith("--text-file="): - with open(g[12:], encoding="utf-8") as f: + with open(g[12:], encoding="utf-8-sig") as f: text += f.read().replace("\n", "") continue if g.startswith("--unicodes="): @@ -3796,6 +3811,8 @@ def main(args=None): for t in font["cmap"].tables: if t.isUnicode(): unicodes.extend(t.cmap.keys()) + if t.format == 14: + unicodes.extend(t.uvsDict.keys()) assert "" not in glyphs log.info("Text: '%s'" % text) diff --git a/Lib/fontTools/svgLib/path/__init__.py b/Lib/fontTools/svgLib/path/__init__.py index 742bc64ce0..043b4dbe1d 100644 --- a/Lib/fontTools/svgLib/path/__init__.py +++ b/Lib/fontTools/svgLib/path/__init__.py @@ -14,6 +14,8 @@ class SVGPath(object): For example, reading from an SVG file and drawing to a Defcon Glyph: + .. code-block:: + import defcon glyph = defcon.Glyph() pen = glyph.getPen() @@ -23,6 +25,8 @@ class SVGPath(object): Or reading from a string containing SVG data, using the alternative 'fromstring' (a class method): + .. code-block:: + data = ' _g_l_y_f.Glyph: return glyph +def _charString_from_SkPath( + path: pathops.Path, charString: T2CharString +) -> T2CharString: + if charString.width == charString.private.defaultWidthX: + width = None + else: + width = charString.width - charString.private.nominalWidthX + t2Pen = T2CharStringPen(width=width, glyphSet=None) + path.draw(t2Pen) + return t2Pen.getCharString(charString.private, charString.globalSubrs) + + def _round_path( path: pathops.Path, round: Callable[[float], float] = otRound ) -> pathops.Path: @@ -90,7 +105,12 @@ def _round_path( return rounded_path -def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path: +def _simplify( + path: pathops.Path, + debugGlyphName: str, + *, + round: Callable[[float], float] = otRound, +) -> pathops.Path: # skia-pathops has a bug where it sometimes fails to simplify paths when there # are float coordinates and control points are very close to one another. # Rounding coordinates to integers works around the bug. @@ -105,7 +125,7 @@ def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path: except pathops.PathOpsError: pass - path = _round_path(path) + path = _round_path(path, round=round) try: path = pathops.simplify(path, clockwise=path.clockwise) log.debug( @@ -124,6 +144,10 @@ def _simplify(path: pathops.Path, debugGlyphName: str) -> pathops.Path: raise AssertionError("Unreachable") +def _same_path(path1: pathops.Path, path2: pathops.Path) -> bool: + return {tuple(c) for c in path1.contours} == {tuple(c) for c in path2.contours} + + def removeTTGlyphOverlaps( glyphName: str, glyphSet: _TTGlyphMapping, @@ -144,7 +168,7 @@ def removeTTGlyphOverlaps( path2 = _simplify(path, glyphName) # replace TTGlyph if simplified path is different (ignoring contour order) - if {tuple(c) for c in path.contours} != {tuple(c) for c in path2.contours}: + if not _same_path(path, path2): glyfTable[glyphName] = glyph = ttfGlyphFromSkPath(path2) # simplified glyph is always unhinted assert not glyph.program @@ -159,42 +183,16 @@ def removeTTGlyphOverlaps( return False -def removeOverlaps( +def _remove_glyf_overlaps( + *, font: ttFont.TTFont, - glyphNames: Optional[Iterable[str]] = None, - removeHinting: bool = True, - ignoreErrors=False, + glyphNames: Iterable[str], + glyphSet: _TTGlyphMapping, + removeHinting: bool, + ignoreErrors: bool, ) -> None: - """Simplify glyphs in TTFont by merging overlapping contours. - - Overlapping components are first decomposed to simple contours, then merged. - - Currently this only works with TrueType fonts with 'glyf' table. - Raises NotImplementedError if 'glyf' table is absent. - - Note that removing overlaps invalidates the hinting. By default we drop hinting - from all glyphs whether or not overlaps are removed from a given one, as it would - look weird if only some glyphs are left (un)hinted. - - Args: - font: input TTFont object, modified in place. - glyphNames: optional iterable of glyph names (str) to remove overlaps from. - By default, all glyphs in the font are processed. - removeHinting (bool): set to False to keep hinting for unmodified glyphs. - ignoreErrors (bool): set to True to ignore errors while removing overlaps, - thus keeping the tricky glyphs unchanged (fonttools/fonttools#2363). - """ - try: - glyfTable = font["glyf"] - except KeyError: - raise NotImplementedError("removeOverlaps currently only works with TTFs") - + glyfTable = font["glyf"] hmtxTable = font["hmtx"] - # wraps the underlying glyf Glyphs, takes care of interfacing with drawing pens - glyphSet = font.getGlyphSet() - - if glyphNames is None: - glyphNames = font.getGlyphOrder() # process all simple glyphs first, then composites with increasing component depth, # so that by the time we test for component intersections the respective base glyphs @@ -225,27 +223,170 @@ def removeOverlaps( log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified)) -def main(args=None): - """Simplify glyphs in TTFont by merging overlapping contours.""" +def _remove_charstring_overlaps( + *, + glyphName: str, + glyphSet: _TTGlyphMapping, + cffFontSet: CFFFontSet, +) -> bool: + path = skPathFromGlyph(glyphName, glyphSet) + + # remove overlaps + path2 = _simplify(path, glyphName, round=noRound) + + # replace TTGlyph if simplified path is different (ignoring contour order) + if not _same_path(path, path2): + charStrings = cffFontSet[0].CharStrings + charStrings[glyphName] = _charString_from_SkPath(path2, charStrings[glyphName]) + return True - import sys + return False - if args is None: - args = sys.argv[1:] - if len(args) < 2: - print( - f"usage: fonttools ttLib.removeOverlaps INPUT.ttf OUTPUT.ttf [GLYPHS ...]" +def _remove_cff_overlaps( + *, + font: ttFont.TTFont, + glyphNames: Iterable[str], + glyphSet: _TTGlyphMapping, + removeHinting: bool, + ignoreErrors: bool, + removeUnusedSubroutines: bool = True, +) -> None: + cffFontSet = font["CFF "].cff + modified = set() + for glyphName in glyphNames: + try: + if _remove_charstring_overlaps( + glyphName=glyphName, + glyphSet=glyphSet, + cffFontSet=cffFontSet, + ): + modified.add(glyphName) + except RemoveOverlapsError: + if not ignoreErrors: + raise + log.error("Failed to remove overlaps for '%s'", glyphName) + + if not modified: + log.debug("No overlaps found in the specified CFF glyphs") + return + + if removeHinting: + cffFontSet.remove_hints() + + if removeUnusedSubroutines: + cffFontSet.remove_unused_subroutines() + + log.debug("Removed overlaps for %s glyphs:\n%s", len(modified), " ".join(modified)) + + +def removeOverlaps( + font: ttFont.TTFont, + glyphNames: Optional[Iterable[str]] = None, + removeHinting: bool = True, + ignoreErrors: bool = False, + *, + removeUnusedSubroutines: bool = True, +) -> None: + """Simplify glyphs in TTFont by merging overlapping contours. + + Overlapping components are first decomposed to simple contours, then merged. + + Currently this only works for fonts with 'glyf' or 'CFF ' tables. + Raises NotImplementedError if 'glyf' or 'CFF ' tables are absent. + + Note that removing overlaps invalidates the hinting. By default we drop hinting + from all glyphs whether or not overlaps are removed from a given one, as it would + look weird if only some glyphs are left (un)hinted. + + Args: + font: input TTFont object, modified in place. + glyphNames: optional iterable of glyph names (str) to remove overlaps from. + By default, all glyphs in the font are processed. + removeHinting (bool): set to False to keep hinting for unmodified glyphs. + ignoreErrors (bool): set to True to ignore errors while removing overlaps, + thus keeping the tricky glyphs unchanged (fonttools/fonttools#2363). + removeUnusedSubroutines (bool): set to False to keep unused subroutines + in CFF table after removing overlaps. Default is to remove them if + any glyphs are modified. + """ + + if "glyf" not in font and "CFF " not in font: + raise NotImplementedError( + "No outline data found in the font: missing 'glyf' or 'CFF ' table" ) - sys.exit(1) - src = args[0] - dst = args[1] - glyphNames = args[2:] or None + if glyphNames is None: + glyphNames = font.getGlyphOrder() + + # Wraps the underlying glyphs, takes care of interfacing with drawing pens + glyphSet = font.getGlyphSet() + + if "glyf" in font: + _remove_glyf_overlaps( + font=font, + glyphNames=glyphNames, + glyphSet=glyphSet, + removeHinting=removeHinting, + ignoreErrors=ignoreErrors, + ) + + if "CFF " in font: + _remove_cff_overlaps( + font=font, + glyphNames=glyphNames, + glyphSet=glyphSet, + removeHinting=removeHinting, + ignoreErrors=ignoreErrors, + removeUnusedSubroutines=removeUnusedSubroutines, + ) - with ttFont.TTFont(src) as f: - removeOverlaps(f, glyphNames) - f.save(dst) + +def main(args=None): + """Simplify glyphs in TTFont by merging overlapping contours.""" + + import argparse + + parser = argparse.ArgumentParser( + "fonttools ttLib.removeOverlaps", description=__doc__ + ) + + parser.add_argument("input", metavar="INPUT.ttf", help="Input font file") + parser.add_argument("output", metavar="OUTPUT.ttf", help="Output font file") + parser.add_argument( + "glyphs", + metavar="GLYPHS", + nargs="*", + help="Optional list of glyph names to remove overlaps from", + ) + parser.add_argument( + "--keep-hinting", + action="store_true", + help="Keep hinting for unmodified glyphs, default is to drop hinting", + ) + parser.add_argument( + "--ignore-errors", + action="store_true", + help="ignore errors while removing overlaps, " + "thus keeping the tricky glyphs unchanged", + ) + parser.add_argument( + "--keep-unused-subroutines", + action="store_true", + help="Keep unused subroutines in CFF table after removing overlaps, " + "default is to remove them if any glyphs are modified", + ) + args = parser.parse_args(args) + + with ttFont.TTFont(args.input) as font: + removeOverlaps( + font=font, + glyphNames=args.glyphs or None, + removeHinting=not args.keep_hinting, + ignoreErrors=args.ignore_errors, + removeUnusedSubroutines=not args.keep_unused_subroutines, + ) + font.save(args.output) if __name__ == "__main__": diff --git a/Lib/fontTools/ttLib/reorderGlyphs.py b/Lib/fontTools/ttLib/reorderGlyphs.py index 3221261f16..fd950ba0e3 100644 --- a/Lib/fontTools/ttLib/reorderGlyphs.py +++ b/Lib/fontTools/ttLib/reorderGlyphs.py @@ -19,9 +19,7 @@ Deque, Iterable, List, - NamedTuple, Tuple, - Union, ) @@ -276,3 +274,11 @@ def reorderGlyphs(font: ttLib.TTFont, new_glyph_order: List[str]): reorder_key = (type(value), getattr(value, "Format", None)) for reorder in _REORDER_RULES.get(reorder_key, []): reorder.apply(font, value) + + if "CFF " in font: + cff_table = font["CFF "] + charstrings = cff_table.cff.topDictIndex[0].CharStrings.charStrings + cff_table.cff.topDictIndex[0].charset = new_glyph_order + cff_table.cff.topDictIndex[0].CharStrings.charStrings = { + k: charstrings.get(k) for k in new_glyph_order + } diff --git a/Lib/fontTools/ttLib/sfnt.py b/Lib/fontTools/ttLib/sfnt.py index b1569423c4..6cc867a4d7 100644 --- a/Lib/fontTools/ttLib/sfnt.py +++ b/Lib/fontTools/ttLib/sfnt.py @@ -1,8 +1,9 @@ """ttLib/sfnt.py -- low-level module to deal with the sfnt file format. Defines two public classes: - SFNTReader - SFNTWriter + +- SFNTReader +- SFNTWriter (Normally you don't have to use these classes explicitly; they are used automatically by ttLib.TTFont.) diff --git a/Lib/fontTools/ttLib/tables/B_A_S_E_.py b/Lib/fontTools/ttLib/tables/B_A_S_E_.py index f468a963a1..0f4b1c3c5a 100644 --- a/Lib/fontTools/ttLib/tables/B_A_S_E_.py +++ b/Lib/fontTools/ttLib/tables/B_A_S_E_.py @@ -2,4 +2,13 @@ class table_B_A_S_E_(BaseTTXConverter): + """Baseline table + + The ``BASE`` table contains information needed to align glyphs in + different scripts, from different fonts, or at different sizes + within the same line of text. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/base + """ + pass diff --git a/Lib/fontTools/ttLib/tables/C_B_D_T_.py b/Lib/fontTools/ttLib/tables/C_B_D_T_.py index 2b87ac8628..0f9924745e 100644 --- a/Lib/fontTools/ttLib/tables/C_B_D_T_.py +++ b/Lib/fontTools/ttLib/tables/C_B_D_T_.py @@ -21,6 +21,16 @@ class table_C_B_D_T_(E_B_D_T_.table_E_B_D_T_): + """Color Bitmap Data table + + The ``CBDT`` table contains color bitmap data for glyphs. It must + be used in concert with the ``CBLC`` table. + + It is backwards-compatible with the monochrome/grayscale ``EBDT`` table. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cbdt + """ + # Change the data locator table being referenced. locatorName = "CBLC" diff --git a/Lib/fontTools/ttLib/tables/C_B_L_C_.py b/Lib/fontTools/ttLib/tables/C_B_L_C_.py index fc3974ece0..0673fb7ed2 100644 --- a/Lib/fontTools/ttLib/tables/C_B_L_C_.py +++ b/Lib/fontTools/ttLib/tables/C_B_L_C_.py @@ -6,4 +6,14 @@ class table_C_B_L_C_(E_B_L_C_.table_E_B_L_C_): + """Color Bitmap Location table + + The ``CBLC`` table contains the locations of color bitmaps for glyphs. It must + be used in concert with the ``CBDT`` table. + + It is backwards-compatible with the monochrome/grayscale ``EBLC`` table. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cblc + """ + dependencies = ["CBDT"] diff --git a/Lib/fontTools/ttLib/tables/C_F_F_.py b/Lib/fontTools/ttLib/tables/C_F_F_.py index c231599e37..3d974ced88 100644 --- a/Lib/fontTools/ttLib/tables/C_F_F_.py +++ b/Lib/fontTools/ttLib/tables/C_F_F_.py @@ -4,6 +4,21 @@ class table_C_F_F_(DefaultTable.DefaultTable): + """Compact Font Format table (version 1) + + The ``CFF`` table embeds a CFF-formatted font. The CFF font format + predates OpenType and could be used as a standalone font file, but the + ``CFF`` table is also used to package CFF fonts into an OpenType + container. + + .. note:: + ``CFF`` has been succeeded by ``CFF2``, which eliminates much of + the redundancy incurred by embedding CFF version 1 in an OpenType + font. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cff + """ + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.cff = cffLib.CFFFontSet() diff --git a/Lib/fontTools/ttLib/tables/C_F_F__2.py b/Lib/fontTools/ttLib/tables/C_F_F__2.py index edbb0b92f7..ff07682d24 100644 --- a/Lib/fontTools/ttLib/tables/C_F_F__2.py +++ b/Lib/fontTools/ttLib/tables/C_F_F__2.py @@ -3,6 +3,19 @@ class table_C_F_F__2(table_C_F_F_): + """Compact Font Format version 2 table + + The ``CFF2`` table contains glyph data for a CFF2-flavored OpenType + font. + + .. note:: + ``CFF2`` is the successor to ``CFF``, and eliminates much of + the redundancy incurred by embedding CFF version 1 in an OpenType + font. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cff2 + """ + def decompile(self, data, otFont): self.cff.decompile(BytesIO(data), otFont, isCFF2=True) assert len(self.cff) == 1, "can't deal with multi-font CFF tables." diff --git a/Lib/fontTools/ttLib/tables/C_O_L_R_.py b/Lib/fontTools/ttLib/tables/C_O_L_R_.py index df857842cc..266a11c41d 100644 --- a/Lib/fontTools/ttLib/tables/C_O_L_R_.py +++ b/Lib/fontTools/ttLib/tables/C_O_L_R_.py @@ -7,11 +7,19 @@ class table_C_O_L_R_(DefaultTable.DefaultTable): - """This table is structured so that you can treat it like a dictionary keyed by glyph name. + """Color table + + The ``COLR`` table defines color presentation of outline glyphs. It must + be used in concert with the ``CPAL`` table, which contains the color + descriptors used. + + This table is structured so that you can treat it like a dictionary keyed by glyph name. ``ttFont['COLR'][]`` will return the color layers for any glyph. ``ttFont['COLR'][] = `` will set the color layers for any glyph. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/colr """ @staticmethod diff --git a/Lib/fontTools/ttLib/tables/C_P_A_L_.py b/Lib/fontTools/ttLib/tables/C_P_A_L_.py index 9fb2074afc..5ec8c843c5 100644 --- a/Lib/fontTools/ttLib/tables/C_P_A_L_.py +++ b/Lib/fontTools/ttLib/tables/C_P_A_L_.py @@ -11,6 +11,15 @@ class table_C_P_A_L_(DefaultTable.DefaultTable): + """Color Palette table + + The ``CPAL`` table contains a set of one or more color palettes. The color + records in each palette can be referenced by the ``COLR`` table to specify + the colors used in a color glyph. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cpal + """ + NO_NAME_ID = 0xFFFF DEFAULT_PALETTE_TYPE = 0 diff --git a/Lib/fontTools/ttLib/tables/D_S_I_G_.py b/Lib/fontTools/ttLib/tables/D_S_I_G_.py index d902a29080..f89cc29e49 100644 --- a/Lib/fontTools/ttLib/tables/D_S_I_G_.py +++ b/Lib/fontTools/ttLib/tables/D_S_I_G_.py @@ -39,6 +39,13 @@ class table_D_S_I_G_(DefaultTable.DefaultTable): + """Digital Signature table + + The ``DSIG`` table contains cryptographic signatures for the font. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/dsig + """ + def decompile(self, data, ttFont): dummy, newData = sstruct.unpack2(DSIG_HeaderFormat, data, self) assert self.ulVersion == 1, "DSIG ulVersion must be 1" diff --git a/Lib/fontTools/ttLib/tables/D__e_b_g.py b/Lib/fontTools/ttLib/tables/D__e_b_g.py index 54449a5fd6..cb1653d79d 100644 --- a/Lib/fontTools/ttLib/tables/D__e_b_g.py +++ b/Lib/fontTools/ttLib/tables/D__e_b_g.py @@ -1,9 +1,15 @@ import json +from textwrap import indent from . import DefaultTable +from fontTools.misc.textTools import tostr class table_D__e_b_g(DefaultTable.DefaultTable): + def __init__(self, tag=None): + DefaultTable.DefaultTable.__init__(self, tag) + self.data = {} + def decompile(self, data, ttFont): self.data = json.loads(data) @@ -11,7 +17,19 @@ def compile(self, ttFont): return json.dumps(self.data).encode("utf-8") def toXML(self, writer, ttFont): - writer.writecdata(json.dumps(self.data, indent=2)) + # make sure json indentation inside CDATA block matches XMLWriter's + data = json.dumps(self.data, indent=len(writer.indentwhite)) + prefix = tostr(writer.indentwhite) * (writer.indentlevel + 1) + # but don't indent the first json line so it's adjacent to `HH", data[:4]) data = data[4:] diff --git a/Lib/fontTools/ttLib/tables/M_A_T_H_.py b/Lib/fontTools/ttLib/tables/M_A_T_H_.py index 011426b52a..35d29e9b13 100644 --- a/Lib/fontTools/ttLib/tables/M_A_T_H_.py +++ b/Lib/fontTools/ttLib/tables/M_A_T_H_.py @@ -2,4 +2,12 @@ class table_M_A_T_H_(BaseTTXConverter): + """Mathematical Typesetting table + + The ``MATH`` table contains a variety of information needed to + typeset glyphs in mathematical formulas and expressions. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/math + """ + pass diff --git a/Lib/fontTools/ttLib/tables/M_E_T_A_.py b/Lib/fontTools/ttLib/tables/M_E_T_A_.py index 445aeb4dea..6a6f8bbf84 100644 --- a/Lib/fontTools/ttLib/tables/M_E_T_A_.py +++ b/Lib/fontTools/ttLib/tables/M_E_T_A_.py @@ -68,6 +68,13 @@ def getLabelString(labelID): class table_M_E_T_A_(DefaultTable.DefaultTable): + """Glyphlets META table + + The ``META`` table is used by Adobe's SING Glyphlets. + + See also https://web.archive.org/web/20080627183635/http://www.adobe.com/devnet/opentype/gdk/topic.html + """ + dependencies = [] def decompile(self, data, ttFont): diff --git a/Lib/fontTools/ttLib/tables/M_V_A_R_.py b/Lib/fontTools/ttLib/tables/M_V_A_R_.py index 8371795eb2..c7e7e90fb4 100644 --- a/Lib/fontTools/ttLib/tables/M_V_A_R_.py +++ b/Lib/fontTools/ttLib/tables/M_V_A_R_.py @@ -2,4 +2,12 @@ class table_M_V_A_R_(BaseTTXConverter): + """Metrics Variations table + + The ``MVAR`` table contains variation information for font-wide + metrics in a variable font. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/mvar + """ + pass diff --git a/Lib/fontTools/ttLib/tables/O_S_2f_2.py b/Lib/fontTools/ttLib/tables/O_S_2f_2.py index 0c739bcc44..9a7e5f70bb 100644 --- a/Lib/fontTools/ttLib/tables/O_S_2f_2.py +++ b/Lib/fontTools/ttLib/tables/O_S_2f_2.py @@ -113,7 +113,14 @@ def fromXML(self, name, attrs, content, ttFont): class table_O_S_2f_2(DefaultTable.DefaultTable): - """the OS/2 table""" + """OS/2 and Windows Metrics table + + The ``OS/2`` table contains a variety of font-wide metrics and + parameters that may be useful to an operating system or other + software for system-integration purposes. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/os2 + """ dependencies = ["head"] diff --git a/Lib/fontTools/ttLib/tables/S_I_N_G_.py b/Lib/fontTools/ttLib/tables/S_I_N_G_.py index 4522c06c6b..1a367a92f2 100644 --- a/Lib/fontTools/ttLib/tables/S_I_N_G_.py +++ b/Lib/fontTools/ttLib/tables/S_I_N_G_.py @@ -20,6 +20,13 @@ class table_S_I_N_G_(DefaultTable.DefaultTable): + """Glyphlets SING table + + The ``SING`` table is used by Adobe's SING Glyphlets. + + See also https://web.archive.org/web/20080627183635/http://www.adobe.com/devnet/opentype/gdk/topic.html + """ + dependencies = [] def decompile(self, data, ttFont): diff --git a/Lib/fontTools/ttLib/tables/S_T_A_T_.py b/Lib/fontTools/ttLib/tables/S_T_A_T_.py index 1769de91b5..86e1271a98 100644 --- a/Lib/fontTools/ttLib/tables/S_T_A_T_.py +++ b/Lib/fontTools/ttLib/tables/S_T_A_T_.py @@ -2,4 +2,14 @@ class table_S_T_A_T_(BaseTTXConverter): + """Style Attributes table + + The ``STAT`` table records stylistic or typeface-design attributes that + differentiate the individual fonts within a font family from one another. + Those attributes can be used to assist users when navigating the style + variations of a variable font or a family of static fonts. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/stat + """ + pass diff --git a/Lib/fontTools/ttLib/tables/S_V_G_.py b/Lib/fontTools/ttLib/tables/S_V_G_.py index ebc2befdfe..76e860c5f6 100644 --- a/Lib/fontTools/ttLib/tables/S_V_G_.py +++ b/Lib/fontTools/ttLib/tables/S_V_G_.py @@ -51,6 +51,14 @@ class table_S_V_G_(DefaultTable.DefaultTable): + """Scalable Vector Graphics table + + The ``SVG`` table contains representations for glyphs in the SVG + image format. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/stat + """ + def decompile(self, data, ttFont): self.docList = [] # Version 0 is the standardized version of the table; and current. diff --git a/Lib/fontTools/ttLib/tables/S__i_l_f.py b/Lib/fontTools/ttLib/tables/S__i_l_f.py index 324ffd0165..876fef3cba 100644 --- a/Lib/fontTools/ttLib/tables/S__i_l_f.py +++ b/Lib/fontTools/ttLib/tables/S__i_l_f.py @@ -343,7 +343,10 @@ class _Object: class table_S__i_l_f(DefaultTable.DefaultTable): - """Silf table support""" + """Graphite Rules table + + See also https://graphite.sil.org/graphite_techAbout#graphite-font-tables + """ def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) diff --git a/Lib/fontTools/ttLib/tables/S__i_l_l.py b/Lib/fontTools/ttLib/tables/S__i_l_l.py index 12b0b8f6cc..61106ea528 100644 --- a/Lib/fontTools/ttLib/tables/S__i_l_l.py +++ b/Lib/fontTools/ttLib/tables/S__i_l_l.py @@ -12,6 +12,11 @@ class table_S__i_l_l(DefaultTable.DefaultTable): + """Graphite Languages table + + See also https://graphite.sil.org/graphite_techAbout#graphite-font-tables + """ + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.langs = {} diff --git a/Lib/fontTools/ttLib/tables/T_S_I_B_.py b/Lib/fontTools/ttLib/tables/T_S_I_B_.py index 8a6c14c444..ea19fcee85 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_B_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_B_.py @@ -1,3 +1,11 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +TSIB contains the source text for the ``BASE`` table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from .T_S_I_V_ import table_T_S_I_V_ diff --git a/Lib/fontTools/ttLib/tables/T_S_I_C_.py b/Lib/fontTools/ttLib/tables/T_S_I_C_.py index 573b3f9c39..14e353cde5 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_C_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_C_.py @@ -1,3 +1,12 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +TSIC contains the source text for the Variation CVT window and data for +the ``cvar`` table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from .otBase import BaseTTXConverter diff --git a/Lib/fontTools/ttLib/tables/T_S_I_D_.py b/Lib/fontTools/ttLib/tables/T_S_I_D_.py index 536ff2f98a..2bb9455efa 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_D_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_D_.py @@ -1,3 +1,11 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +TSID contains the source text for the ``GDEF`` table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from .T_S_I_V_ import table_T_S_I_V_ diff --git a/Lib/fontTools/ttLib/tables/T_S_I_J_.py b/Lib/fontTools/ttLib/tables/T_S_I_J_.py index bc8fe92aac..379b949c6c 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_J_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_J_.py @@ -1,3 +1,11 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +TSIJ contains the source text for the ``JSTF`` table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from .T_S_I_V_ import table_T_S_I_V_ diff --git a/Lib/fontTools/ttLib/tables/T_S_I_P_.py b/Lib/fontTools/ttLib/tables/T_S_I_P_.py index 1abc02590c..8b17869704 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_P_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_P_.py @@ -1,3 +1,11 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +TSIP contains the source text for the ``GPOS`` table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from .T_S_I_V_ import table_T_S_I_V_ diff --git a/Lib/fontTools/ttLib/tables/T_S_I_S_.py b/Lib/fontTools/ttLib/tables/T_S_I_S_.py index 667eb0e534..91f36e7c01 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_S_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_S_.py @@ -1,3 +1,11 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +TSIS contains the source text for the ``GSUB`` table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from .T_S_I_V_ import table_T_S_I_V_ diff --git a/Lib/fontTools/ttLib/tables/T_S_I_V_.py b/Lib/fontTools/ttLib/tables/T_S_I_V_.py index d7aec4589c..32af783f26 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I_V_.py +++ b/Lib/fontTools/ttLib/tables/T_S_I_V_.py @@ -1,3 +1,9 @@ +""" TSI{B,C,D,J,P,S,V} are private tables used by Microsoft Visual TrueType (VTT) +tool to store its table source data. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables +""" + from fontTools.misc.textTools import strjoin, tobytes, tostr from . import asciiTable diff --git a/Lib/fontTools/ttLib/tables/T_S_I__0.py b/Lib/fontTools/ttLib/tables/T_S_I__0.py index 77905822a8..d60e783c60 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__0.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__0.py @@ -1,14 +1,20 @@ -""" 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 programs and 'extra' programs ('fpgm', 'prep', and 'cvt') that are contained in the TSI1 table. + +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" @@ -23,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__1.py b/Lib/fontTools/ttLib/tables/T_S_I__1.py index a9d04a09b0..b0b851cc78 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__1.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__1.py @@ -3,6 +3,8 @@ TSI1 contains the text of the glyph programs in the form of low-level assembly code, as well as the 'extra' programs 'fpgm', 'ppgm' (i.e. 'prep'), and 'cvt'. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables """ from . import DefaultTable diff --git a/Lib/fontTools/ttLib/tables/T_S_I__2.py b/Lib/fontTools/ttLib/tables/T_S_I__2.py index 163ef45226..63608c6041 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__2.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__2.py @@ -4,6 +4,8 @@ TSI2 is the index table containing the lengths and offsets for the glyph programs that are contained in the TSI3 table. It uses the same format as the TSI0 table. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables """ from fontTools import ttLib diff --git a/Lib/fontTools/ttLib/tables/T_S_I__3.py b/Lib/fontTools/ttLib/tables/T_S_I__3.py index 604a7f0beb..e866826db4 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__3.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__3.py @@ -2,6 +2,8 @@ tool to store its hinting source data. TSI3 contains the text of the glyph programs in the form of 'VTTTalk' code. + +See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables """ from fontTools import ttLib diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py index d86798695c..6afd76832f 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__5.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py @@ -1,25 +1,38 @@ -""" 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. + +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/T_T_F_A_.py b/Lib/fontTools/ttLib/tables/T_T_F_A_.py index e3cf2db2d7..4b6b06871a 100644 --- a/Lib/fontTools/ttLib/tables/T_T_F_A_.py +++ b/Lib/fontTools/ttLib/tables/T_T_F_A_.py @@ -2,4 +2,13 @@ class table_T_T_F_A_(asciiTable.asciiTable): + """ttfautohint parameters table + + The ``TTFA`` table is used by the free-software `ttfautohint` program + to record the parameters that `ttfautohint` was called with when it + was used to auto-hint the font. + + See also http://freetype.org/ttfautohint/doc/ttfautohint.html#miscellaneous-1 + """ + pass diff --git a/Lib/fontTools/ttLib/tables/TupleVariation.py b/Lib/fontTools/ttLib/tables/TupleVariation.py index a98bca2e0e..bd6217e2ed 100644 --- a/Lib/fontTools/ttLib/tables/TupleVariation.py +++ b/Lib/fontTools/ttLib/tables/TupleVariation.py @@ -129,7 +129,9 @@ def fromXML(self, name, attrs, _content): else: log.warning("bad delta format: %s" % ", ".join(sorted(attrs.keys()))) - def compile(self, axisTags, sharedCoordIndices={}, pointData=None): + def compile( + self, axisTags, sharedCoordIndices={}, pointData=None, *, optimizeSize=True + ): assert set(self.axes.keys()) <= set(axisTags), ( "Unknown axis tag found.", self.axes.keys(), @@ -161,7 +163,7 @@ def compile(self, axisTags, sharedCoordIndices={}, pointData=None): flags |= PRIVATE_POINT_NUMBERS auxData.append(pointData) - auxData.append(self.compileDeltas()) + auxData.append(self.compileDeltas(optimizeSize=optimizeSize)) auxData = b"".join(auxData) tupleData.insert(0, struct.pack(">HH", len(auxData), flags)) @@ -322,7 +324,7 @@ def decompilePoints_(numPoints, data, offset, tableTag): ) return (result, pos) - def compileDeltas(self): + def compileDeltas(self, optimizeSize=True): deltaX = [] deltaY = [] if self.getCoordWidth() == 2: @@ -337,12 +339,12 @@ def compileDeltas(self): continue deltaX.append(c) bytearr = bytearray() - self.compileDeltaValues_(deltaX, bytearr) - self.compileDeltaValues_(deltaY, bytearr) + self.compileDeltaValues_(deltaX, bytearr, optimizeSize=optimizeSize) + self.compileDeltaValues_(deltaY, bytearr, optimizeSize=optimizeSize) return bytearr @staticmethod - def compileDeltaValues_(deltas, bytearr=None): + def compileDeltaValues_(deltas, bytearr=None, *, optimizeSize=True): """[value1, value2, value3, ...] --> bytearray Emits a sequence of runs. Each run starts with a @@ -360,18 +362,40 @@ def compileDeltaValues_(deltas, bytearr=None): """ # Explaining the format because the 'gvar' spec is hard to understand. if bytearr is None: bytearr = bytearray() + pos = 0 numDeltas = len(deltas) - while pos < numDeltas: - value = deltas[pos] - if value == 0: + + if optimizeSize: + while pos < numDeltas: + value = deltas[pos] + if value == 0: + pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) + elif -128 <= value <= 127: + pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) + elif -32768 <= value <= 32767: + pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) + else: + pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) + else: + minVal, maxVal = min(deltas), max(deltas) + if minVal == 0 == maxVal: pos = TupleVariation.encodeDeltaRunAsZeroes_(deltas, pos, bytearr) - elif -128 <= value <= 127: - pos = TupleVariation.encodeDeltaRunAsBytes_(deltas, pos, bytearr) - elif -32768 <= value <= 32767: - pos = TupleVariation.encodeDeltaRunAsWords_(deltas, pos, bytearr) + elif -128 <= minVal <= maxVal <= 127: + pos = TupleVariation.encodeDeltaRunAsBytes_( + deltas, pos, bytearr, optimizeSize=False + ) + elif -32768 <= minVal <= maxVal <= 32767: + pos = TupleVariation.encodeDeltaRunAsWords_( + deltas, pos, bytearr, optimizeSize=False + ) else: - pos = TupleVariation.encodeDeltaRunAsLongs_(deltas, pos, bytearr) + pos = TupleVariation.encodeDeltaRunAsLongs_( + deltas, pos, bytearr, optimizeSize=False + ) + + assert pos == numDeltas, (pos, numDeltas) + return bytearr @staticmethod @@ -389,7 +413,7 @@ def encodeDeltaRunAsZeroes_(deltas, offset, bytearr): return pos @staticmethod - def encodeDeltaRunAsBytes_(deltas, offset, bytearr): + def encodeDeltaRunAsBytes_(deltas, offset, bytearr, optimizeSize=True): pos = offset numDeltas = len(deltas) while pos < numDeltas: @@ -404,7 +428,12 @@ def encodeDeltaRunAsBytes_(deltas, offset, bytearr): # (04 0F 0F 00 0F 0F) when storing the zero value # literally, but 7 bytes (01 0F 0F 80 01 0F 0F) # when starting a new run. - if value == 0 and pos + 1 < numDeltas and deltas[pos + 1] == 0: + if ( + optimizeSize + and value == 0 + and pos + 1 < numDeltas + and deltas[pos + 1] == 0 + ): break pos += 1 runLength = pos - offset @@ -419,7 +448,7 @@ def encodeDeltaRunAsBytes_(deltas, offset, bytearr): return pos @staticmethod - def encodeDeltaRunAsWords_(deltas, offset, bytearr): + def encodeDeltaRunAsWords_(deltas, offset, bytearr, optimizeSize=True): pos = offset numDeltas = len(deltas) while pos < numDeltas: @@ -432,7 +461,7 @@ def encodeDeltaRunAsWords_(deltas, offset, bytearr): # storing the zero literally (42 66 66 00 00 77 77), # and equally 7 bytes when starting a new run # (40 66 66 80 40 77 77). - if value == 0: + if optimizeSize and value == 0: break # Within a word-encoded run of deltas, a single value @@ -442,7 +471,8 @@ def encodeDeltaRunAsWords_(deltas, offset, bytearr): # the value literally (42 66 66 00 02 77 77), but 8 bytes # when starting a new run (40 66 66 00 02 40 77 77). if ( - (-128 <= value <= 127) + optimizeSize + and (-128 <= value <= 127) and pos + 1 < numDeltas and (-128 <= deltas[pos + 1] <= 127) ): @@ -470,12 +500,12 @@ def encodeDeltaRunAsWords_(deltas, offset, bytearr): return pos @staticmethod - def encodeDeltaRunAsLongs_(deltas, offset, bytearr): + def encodeDeltaRunAsLongs_(deltas, offset, bytearr, optimizeSize=True): pos = offset numDeltas = len(deltas) while pos < numDeltas: value = deltas[pos] - if -32768 <= value <= 32767: + if optimizeSize and -32768 <= value <= 32767: break pos += 1 runLength = pos - offset @@ -677,7 +707,13 @@ def compileSharedTuples( def compileTupleVariationStore( - variations, pointCount, axisTags, sharedTupleIndices, useSharedPoints=True + variations, + pointCount, + axisTags, + sharedTupleIndices, + useSharedPoints=True, + *, + optimizeSize=True, ): # pointCount is actually unused. Keeping for API compat. del pointCount @@ -733,7 +769,9 @@ def key(pn): ] for v, p in zip(variations, pointDatas): - thisTuple, thisData = v.compile(axisTags, sharedTupleIndices, pointData=p) + thisTuple, thisData = v.compile( + axisTags, sharedTupleIndices, pointData=p, optimizeSize=optimizeSize + ) tuples.append(thisTuple) data.append(thisData) diff --git a/Lib/fontTools/ttLib/tables/V_A_R_C_.py b/Lib/fontTools/ttLib/tables/V_A_R_C_.py index 5a00887160..afc1497d14 100644 --- a/Lib/fontTools/ttLib/tables/V_A_R_C_.py +++ b/Lib/fontTools/ttLib/tables/V_A_R_C_.py @@ -2,4 +2,11 @@ class table_V_A_R_C_(BaseTTXConverter): + """Variable Components table + + The ``VARC`` table contains variation information for composite glyphs. + + See also https://github.com/harfbuzz/boring-expansion-spec/blob/main/VARC.md + """ + pass diff --git a/Lib/fontTools/ttLib/tables/V_D_M_X_.py b/Lib/fontTools/ttLib/tables/V_D_M_X_.py index 0632173cd9..6f8abc46de 100644 --- a/Lib/fontTools/ttLib/tables/V_D_M_X_.py +++ b/Lib/fontTools/ttLib/tables/V_D_M_X_.py @@ -37,6 +37,14 @@ class table_V_D_M_X_(DefaultTable.DefaultTable): + """Vertical Device Metrics table + + The ``VDMX`` table records changes to the vertical glyph minima + and maxima that result from Truetype instructions. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/vdmx + """ + def decompile(self, data, ttFont): pos = 0 # track current position from to start of VDMX table dummy, data = sstruct.unpack2(VDMX_HeaderFmt, data, self) diff --git a/Lib/fontTools/ttLib/tables/V_O_R_G_.py b/Lib/fontTools/ttLib/tables/V_O_R_G_.py index b08737b224..2c53acba43 100644 --- a/Lib/fontTools/ttLib/tables/V_O_R_G_.py +++ b/Lib/fontTools/ttLib/tables/V_O_R_G_.py @@ -4,11 +4,18 @@ class table_V_O_R_G_(DefaultTable.DefaultTable): - """This table is structured so that you can treat it like a dictionary keyed by glyph name. + """Vertical Origin table + + The ``VORG`` table contains the vertical origin of each glyph + in a `CFF` or `CFF2` font. + + This table is structured so that you can treat it like a dictionary keyed by glyph name. ``ttFont['VORG'][]`` will return the vertical origin for any glyph. ``ttFont['VORG'][] = `` will set the vertical origin for any glyph. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/vorg """ def decompile(self, data, ttFont): diff --git a/Lib/fontTools/ttLib/tables/V_V_A_R_.py b/Lib/fontTools/ttLib/tables/V_V_A_R_.py index a3665fea5e..c0c94e348b 100644 --- a/Lib/fontTools/ttLib/tables/V_V_A_R_.py +++ b/Lib/fontTools/ttLib/tables/V_V_A_R_.py @@ -2,4 +2,12 @@ class table_V_V_A_R_(BaseTTXConverter): + """Vertical Metrics Variations table + + The ``VVAR`` table contains variation data for per-glyph vertical metrics + in a variable font. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/vvar + """ + pass 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/_a_n_k_r.py b/Lib/fontTools/ttLib/tables/_a_n_k_r.py index d1062ecc7b..5466d42d41 100644 --- a/Lib/fontTools/ttLib/tables/_a_n_k_r.py +++ b/Lib/fontTools/ttLib/tables/_a_n_k_r.py @@ -2,11 +2,12 @@ class table__a_n_k_r(BaseTTXConverter): - """ + """Anchor Point table + The anchor point table provides a way to define anchor points. These are points within the coordinate space of a given glyph, independent of the control points used to render the glyph. - Anchor points are used in conjunction with the 'kerx' table. + Anchor points are used in conjunction with the ``kerx`` table. See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ankr.html """ diff --git a/Lib/fontTools/ttLib/tables/_a_v_a_r.py b/Lib/fontTools/ttLib/tables/_a_v_a_r.py index 6ea4132b3d..8b96bcef38 100644 --- a/Lib/fontTools/ttLib/tables/_a_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_a_v_a_r.py @@ -22,7 +22,7 @@ class table__a_v_a_r(BaseTTXConverter): - """Axis Variations Table + """Axis Variations table This class represents the ``avar`` table of a variable font. The object has one substantive attribute, ``segments``, which maps axis tags to a segments dictionary:: @@ -43,6 +43,8 @@ class table__a_v_a_r(BaseTTXConverter): ``avar`` segment mapping must contain the entries ``-1.0: -1.0, 0.0: 0.0, 1.0: 1.0``. fontTools does not enforce this, so it is your responsibility to ensure that mappings are valid. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/avar """ dependencies = ["fvar"] @@ -143,7 +145,9 @@ def fromXML(self, name, attrs, content, ttFont): def renormalizeLocation(self, location, font): - if self.majorVersion not in (1, 2): + majorVersion = getattr(self, "majorVersion", 1) + + if majorVersion not in (1, 2): raise NotImplementedError("Unknown avar table version") avarSegments = self.segments @@ -154,7 +158,7 @@ def renormalizeLocation(self, location, font): value = piecewiseLinearMap(value, avarMapping) mappedLocation[axisTag] = value - if self.majorVersion < 2: + if majorVersion < 2: return mappedLocation # Version 2 diff --git a/Lib/fontTools/ttLib/tables/_b_s_l_n.py b/Lib/fontTools/ttLib/tables/_b_s_l_n.py index 8e266fa54d..85796b0a04 100644 --- a/Lib/fontTools/ttLib/tables/_b_s_l_n.py +++ b/Lib/fontTools/ttLib/tables/_b_s_l_n.py @@ -3,4 +3,13 @@ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html class table__b_s_l_n(BaseTTXConverter): + """Baseline table + + The AAT ``bsln`` table is similar in purpose to the OpenType ``BASE`` + table; it stores per-script baselines to support automatic alignment + of lines of text. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_c_i_d_g.py b/Lib/fontTools/ttLib/tables/_c_i_d_g.py index f11901baeb..c283e5a4dd 100644 --- a/Lib/fontTools/ttLib/tables/_c_i_d_g.py +++ b/Lib/fontTools/ttLib/tables/_c_i_d_g.py @@ -3,7 +3,9 @@ class table__c_i_d_g(BaseTTXConverter): - """The AAT ``cidg`` table has almost the same structure as ``gidc``, + """CID to Glyph ID table + + The AAT ``cidg`` table has almost the same structure as ``gidc``, just mapping CIDs to GlyphIDs instead of the reverse direction. It is useful for fonts that may be used by a PDF renderer in lieu of @@ -14,6 +16,9 @@ class table__c_i_d_g(BaseTTXConverter): obsoleted by ``cidg``. For example, the first font in ``/System/Library/Fonts/PingFang.ttc`` - (which Apple ships pre-installed on MacOS 10.12.6) has a ``cidg`` table.""" + (which Apple ships pre-installed on MacOS 10.12.6) has a ``cidg`` table. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gcid.html + """ pass diff --git a/Lib/fontTools/ttLib/tables/_c_m_a_p.py b/Lib/fontTools/ttLib/tables/_c_m_a_p.py index 484c331cb7..7fad1a2d85 100644 --- a/Lib/fontTools/ttLib/tables/_c_m_a_p.py +++ b/Lib/fontTools/ttLib/tables/_c_m_a_p.py @@ -54,6 +54,8 @@ class table__c_m_a_p(DefaultTable.DefaultTable): cmap = newTable("cmap") cmap.tableVersion = 0 cmap.tables = [cmap4_0_3] + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cmap """ def getcmap(self, platformID, platEncID): @@ -139,6 +141,17 @@ def buildReversed(self): result.setdefault(name, set()).add(codepoint) return result + def buildReversedMin(self): + result = {} + for subtable in self.tables: + if subtable.isUnicode(): + for codepoint, name in subtable.cmap.items(): + if name in result: + result[name] = min(result[name], codepoint) + else: + result[name] = codepoint + return result + def decompile(self, data, ttFont): tableVersion, numSubTables = struct.unpack(">HH", data[:4]) self.tableVersion = int(tableVersion) @@ -1160,13 +1173,15 @@ def decompile(self, data, ttFont): charCodes = [] gids = [] pos = 0 + groups = array.array("I", data[: self.nGroups * 12]) + if sys.byteorder != "big": + groups.byteswap() for i in range(self.nGroups): - startCharCode, endCharCode, glyphID = struct.unpack( - ">LLL", data[pos : pos + 12] - ) - pos += 12 + startCharCode = groups[i * 3] + endCharCode = groups[i * 3 + 1] + glyphID = groups[i * 3 + 2] lenGroup = 1 + endCharCode - startCharCode - charCodes.extend(list(range(startCharCode, endCharCode + 1))) + charCodes.extend(range(startCharCode, endCharCode + 1)) gids.extend(self._computeGIDs(glyphID, lenGroup)) self.data = data = None self.cmap = _make_map(self.ttFont, charCodes, gids) @@ -1297,7 +1312,7 @@ def __init__(self, format=12): cmap_format_12_or_13.__init__(self, format) def _computeGIDs(self, startingGlyph, numberOfGlyphs): - return list(range(startingGlyph, startingGlyph + numberOfGlyphs)) + return range(startingGlyph, startingGlyph + numberOfGlyphs) def _IsInSameRun(self, glyphID, lastGlyphID, charCode, lastCharCode): return (glyphID == 1 + lastGlyphID) and (charCode == 1 + lastCharCode) diff --git a/Lib/fontTools/ttLib/tables/_c_v_a_r.py b/Lib/fontTools/ttLib/tables/_c_v_a_r.py index 6ea44dbab3..872710c8f0 100644 --- a/Lib/fontTools/ttLib/tables/_c_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_c_v_a_r.py @@ -24,6 +24,14 @@ class table__c_v_a_r(DefaultTable.DefaultTable): + """Control Value Table (CVT) variations table + + The ``cvar`` table contains variations for the values in a ``cvt`` + table. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cvar + """ + dependencies = ["cvt ", "fvar"] def __init__(self, tag=None): diff --git a/Lib/fontTools/ttLib/tables/_c_v_t.py b/Lib/fontTools/ttLib/tables/_c_v_t.py index 7f94677522..51e2f78df8 100644 --- a/Lib/fontTools/ttLib/tables/_c_v_t.py +++ b/Lib/fontTools/ttLib/tables/_c_v_t.py @@ -5,6 +5,14 @@ class table__c_v_t(DefaultTable.DefaultTable): + """Control Value Table + + The Control Value Table holds a list of values that can be referenced + by TrueType font instructions. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/cvt + """ + def decompile(self, data, ttFont): values = array.array("h") values.frombytes(data) @@ -13,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_e_a_t.py b/Lib/fontTools/ttLib/tables/_f_e_a_t.py index c9a48eff06..8e279db005 100644 --- a/Lib/fontTools/ttLib/tables/_f_e_a_t.py +++ b/Lib/fontTools/ttLib/tables/_f_e_a_t.py @@ -2,11 +2,14 @@ class table__f_e_a_t(BaseTTXConverter): - """The feature name table is an AAT (Apple Advanced Typography) table for + """Feature name table + + The feature name table is an AAT (Apple Advanced Typography) table for storing font features, settings, and their human-readable names. It should not be confused with the ``Feat`` table or the OpenType Layout ``GSUB``/``GPOS`` - tables. See `Feature Name Table `_ - in the TrueType Reference Manual for more information on the structure and - purpose of this table.""" + tables. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6feat.html + """ pass diff --git a/Lib/fontTools/ttLib/tables/_f_p_g_m.py b/Lib/fontTools/ttLib/tables/_f_p_g_m.py index df23041d65..c21a9d4b68 100644 --- a/Lib/fontTools/ttLib/tables/_f_p_g_m.py +++ b/Lib/fontTools/ttLib/tables/_f_p_g_m.py @@ -3,13 +3,26 @@ class table__f_p_g_m(DefaultTable.DefaultTable): + """Font Program table + + The ``fpgm`` table typically contains function defintions that are + used by font instructions. This Font Program is similar to the Control + Value Program that is stored in the ``prep`` table, but + the ``fpgm`` table is only executed one time, when the font is first + used. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/fpgm + """ + def decompile(self, data, ttFont): program = ttProgram.Program() program.fromBytecode(data) 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/_f_v_a_r.py b/Lib/fontTools/ttLib/tables/_f_v_a_r.py index a3bdacd4cc..f2536cb288 100644 --- a/Lib/fontTools/ttLib/tables/_f_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_f_v_a_r.py @@ -43,6 +43,14 @@ class table__f_v_a_r(DefaultTable.DefaultTable): + """FonT Variations table + + The ``fvar`` table contains records of the variation axes and of the + named instances in a variable font. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html + """ + dependencies = ["name"] def __init__(self, tag=None): diff --git a/Lib/fontTools/ttLib/tables/_g_a_s_p.py b/Lib/fontTools/ttLib/tables/_g_a_s_p.py index 10c32a87f4..1f605771e9 100644 --- a/Lib/fontTools/ttLib/tables/_g_a_s_p.py +++ b/Lib/fontTools/ttLib/tables/_g_a_s_p.py @@ -10,6 +10,14 @@ class table__g_a_s_p(DefaultTable.DefaultTable): + """Grid-fitting and Scan-conversion Procedure table + + The ``gasp`` table defines the preferred rasterization settings for + the font when rendered on monochrome and greyscale output devices. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/gasp + """ + def decompile(self, data, ttFont): self.version, numRanges = struct.unpack(">HH", data[:4]) assert 0 <= self.version <= 1, "unknown 'gasp' format: %s" % self.version diff --git a/Lib/fontTools/ttLib/tables/_g_c_i_d.py b/Lib/fontTools/ttLib/tables/_g_c_i_d.py index 2e746c846f..9d6d6ae425 100644 --- a/Lib/fontTools/ttLib/tables/_g_c_i_d.py +++ b/Lib/fontTools/ttLib/tables/_g_c_i_d.py @@ -3,4 +3,11 @@ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gcid.html class table__g_c_i_d(BaseTTXConverter): + """Glyph ID to CID table + + The AAT ``gcid`` table stores glyphID-to-CID mappings. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gcid.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index fa11cf8f47..ea46c9f797 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -54,7 +54,7 @@ class table__g_l_y_f(DefaultTable.DefaultTable): - """Glyph Data Table + """Glyph Data table This class represents the `glyf `_ table, which contains outlines for glyphs in TrueType format. In many cases, @@ -134,6 +134,8 @@ def ensureDecompiled(self, recurse=False): glyph.expand(self) def compile(self, ttFont): + optimizeSpeed = ttFont.cfg[ttLib.OPTIMIZE_FONT_SPEED] + self.axisTags = ( [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else [] ) @@ -148,7 +150,12 @@ def compile(self, ttFont): boundsDone = set() for glyphName in self.glyphOrder: glyph = self.glyphs[glyphName] - glyphData = glyph.compile(self, recalcBBoxes, boundsDone=boundsDone) + glyphData = glyph.compile( + self, + recalcBBoxes, + boundsDone=boundsDone, + optimizeSize=not optimizeSpeed, + ) if padding > 1: glyphData = pad(glyphData, size=padding) locations.append(currentLocation) @@ -713,7 +720,9 @@ def expand(self, glyfTable): else: self.decompileCoordinates(data) - def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): + def compile( + self, glyfTable, recalcBBoxes=True, *, boundsDone=None, optimizeSize=True + ): if hasattr(self, "data"): if recalcBBoxes: # must unpack glyph in order to recalculate bounding box @@ -730,7 +739,7 @@ def compile(self, glyfTable, recalcBBoxes=True, *, boundsDone=None): if self.isComposite(): data = data + self.compileComponents(glyfTable) else: - data = data + self.compileCoordinates() + data = data + self.compileCoordinates(optimizeSize=optimizeSize) return data def toXML(self, writer, ttFont): @@ -976,7 +985,7 @@ def compileComponents(self, glyfTable): data = data + struct.pack(">h", len(instructions)) + instructions return data - def compileCoordinates(self): + def compileCoordinates(self, *, optimizeSize=True): assert len(self.coordinates) == len(self.flags) data = [] endPtsOfContours = array.array("H", self.endPtsOfContours) @@ -991,9 +1000,12 @@ def compileCoordinates(self): deltas.toInt() deltas.absoluteToRelative() - # TODO(behdad): Add a configuration option for this? - deltas = self.compileDeltasGreedy(self.flags, deltas) - # deltas = self.compileDeltasOptimal(self.flags, deltas) + if optimizeSize: + # TODO(behdad): Add a configuration option for this? + deltas = self.compileDeltasGreedy(self.flags, deltas) + # deltas = self.compileDeltasOptimal(self.flags, deltas) + else: + deltas = self.compileDeltasForSpeed(self.flags, deltas) data.extend(deltas) return b"".join(data) @@ -1110,6 +1122,63 @@ def compileDeltasOptimal(self, flags, deltas): return (compressedFlags, compressedXs, compressedYs) + def compileDeltasForSpeed(self, flags, deltas): + # uses widest representation needed, for all deltas. + compressedFlags = bytearray() + compressedXs = bytearray() + compressedYs = bytearray() + + # Compute the necessary width for each axis + xs = [d[0] for d in deltas] + ys = [d[1] for d in deltas] + minX, minY, maxX, maxY = min(xs), min(ys), max(xs), max(ys) + xZero = minX == 0 and maxX == 0 + yZero = minY == 0 and maxY == 0 + xShort = -255 <= minX <= maxX <= 255 + yShort = -255 <= minY <= maxY <= 255 + + lastflag = None + repeat = 0 + for flag, (x, y) in zip(flags, deltas): + # Oh, the horrors of TrueType + # do x + if xZero: + flag = flag | flagXsame + elif xShort: + flag = flag | flagXShort + if x > 0: + flag = flag | flagXsame + else: + x = -x + compressedXs.append(x) + else: + compressedXs.extend(struct.pack(">h", x)) + # do y + if yZero: + flag = flag | flagYsame + elif yShort: + flag = flag | flagYShort + if y > 0: + flag = flag | flagYsame + else: + y = -y + compressedYs.append(y) + else: + compressedYs.extend(struct.pack(">h", y)) + # handle repeating flags + if flag == lastflag and repeat != 255: + repeat = repeat + 1 + if repeat == 1: + compressedFlags.append(flag) + else: + compressedFlags[-2] = flag | flagRepeat + compressedFlags[-1] = repeat + else: + repeat = 0 + compressedFlags.append(flag) + lastflag = flag + return (compressedFlags, compressedXs, compressedYs) + def recalcBounds(self, glyfTable, *, boundsDone=None): """Recalculates the bounds of the glyph. @@ -1123,7 +1192,7 @@ def recalcBounds(self, glyfTable, *, boundsDone=None): ): return try: - coords, endPts, flags = self.getCoordinates(glyfTable) + coords, endPts, flags = self.getCoordinates(glyfTable, round=otRound) self.xMin, self.yMin, self.xMax, self.yMax = coords.calcIntBounds() except NotImplementedError: pass @@ -1142,9 +1211,7 @@ def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): Return True if bounds were calculated, False otherwise. """ for compo in self.components: - if hasattr(compo, "firstPt") or hasattr(compo, "transform"): - return False - if not float(compo.x).is_integer() or not float(compo.y).is_integer(): + if not compo._hasOnlyIntegerTranslate(): return False # All components are untransformed and have an integer x/y translate @@ -1158,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 @@ -1177,7 +1244,7 @@ def isComposite(self): else: return self.numberOfContours == -1 - def getCoordinates(self, glyfTable): + def getCoordinates(self, glyfTable, *, round=noRound): """Return the coordinates, end points and flags This method returns three values: A :py:class:`GlyphCoordinates` object, @@ -1203,13 +1270,23 @@ def getCoordinates(self, glyfTable): for compo in self.components: g = glyfTable[compo.glyphName] try: - coordinates, endPts, flags = g.getCoordinates(glyfTable) + coordinates, endPts, flags = g.getCoordinates( + glyfTable, round=round + ) except RecursionError: raise ttLib.TTLibError( "glyph '%s' contains a recursive component reference" % compo.glyphName ) coordinates = GlyphCoordinates(coordinates) + # if asked to round e.g. while computing bboxes, it's important we + # do it immediately before a component transform is applied to a + # simple glyph's coordinates in case these might still contain floats; + # 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: + coordinates.toInt(round=round) if hasattr(compo, "firstPt"): # component uses two reference points: we apply the transform _before_ # computing the offset between the points @@ -1404,6 +1481,7 @@ def draw(self, pen, glyfTable, offset=0): pen.addComponent(glyphName, transform) return + self.expand(glyfTable) coordinates, endPts, flags = self.getCoordinates(glyfTable) if offset: coordinates = coordinates.copy() @@ -1865,6 +1943,18 @@ def __ne__(self, other): result = self.__eq__(other) return result if result is NotImplemented else not result + def _hasOnlyIntegerTranslate(self): + """Return True if it's a 'simple' component. + + That is, it has no anchor points and no transform other than integer translate. + """ + return ( + not hasattr(self, "firstPt") + and not hasattr(self, "transform") + and float(self.x).is_integer() + and float(self.y).is_integer() + ) + class GlyphCoordinates(object): """A list of glyph coordinates. diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 044f65f716..07d3befb7a 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -3,6 +3,8 @@ from fontTools.misc import sstruct from fontTools.misc.textTools import safeEval from fontTools.misc.lazyTools import LazyDict +from fontTools.ttLib import OPTIMIZE_FONT_SPEED +from fontTools.ttLib.tables.TupleVariation import TupleVariation from . import DefaultTable import array import itertools @@ -11,10 +13,7 @@ import sys import fontTools.ttLib.tables.TupleVariation as tv - log = logging.getLogger(__name__) -TupleVariation = tv.TupleVariation - # https://www.microsoft.com/typography/otspec/gvar.htm # https://www.microsoft.com/typography/otspec/otvarcommonformats.htm @@ -25,23 +24,39 @@ # 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): + """Glyph Variations table + + The ``gvar`` table provides the per-glyph variation data that + describe how glyph outlines in the ``glyf`` table change across + the variation space that is defined for the font in the ``fvar`` + table. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/gvar + """ + dependencies = ["fvar", "glyf"] + gid_size = 2 def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) @@ -49,6 +64,7 @@ def __init__(self, tag=None): self.variations = {} def compile(self, ttFont): + axisTags = [axis.axisTag for axis in ttFont["fvar"].axes] sharedTuples = tv.compileSharedTuples( axisTags, itertools.chain(*self.variations.values()) @@ -64,25 +80,31 @@ 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) def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): + optimizeSpeed = ttFont.cfg[OPTIMIZE_FONT_SPEED] result = [] glyf = ttFont["glyf"] for glyphName in ttFont.getGlyphOrder(): @@ -93,7 +115,12 @@ def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): pointCountUnused = 0 # pointCount is actually unused by compileGlyph result.append( compileGlyph_( - variations, pointCountUnused, axisTags, sharedCoordIndices + self.gid_size, + variations, + pointCountUnused, + axisTags, + sharedCoordIndices, + optimizeSize=not optimizeSpeed, ) ) return result @@ -101,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( @@ -131,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 @@ -248,22 +287,43 @@ def getNumPoints_(glyph): return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS -def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices): +def compileGlyph_( + dataOffsetSize, + variations, + pointCount, + axisTags, + sharedCoordIndices, + *, + optimizeSize=True, +): + assert dataOffsetSize in (2, 3) tupleVariationCount, tuples, data = tv.compileTupleVariationStore( - variations, pointCount, axisTags, sharedCoordIndices + 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", @@ -272,6 +332,6 @@ def decompileGlyph_(pointCount, sharedTuples, axisTags, data): pointCount, sharedTuples, data, - 4, + 2 + dataOffsetSize, offsetToData, ) diff --git a/Lib/fontTools/ttLib/tables/_h_d_m_x.py b/Lib/fontTools/ttLib/tables/_h_d_m_x.py index b6d56a7e70..1ec913de15 100644 --- a/Lib/fontTools/ttLib/tables/_h_d_m_x.py +++ b/Lib/fontTools/ttLib/tables/_h_d_m_x.py @@ -31,6 +31,14 @@ def keys(self): class table__h_d_m_x(DefaultTable.DefaultTable): + """Horizontal Device Metrics table + + The ``hdmx`` table is an optional table that stores advance widths for + glyph outlines at specified pixel sizes. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/hdmx + """ + def decompile(self, data, ttFont): numGlyphs = ttFont["maxp"].numGlyphs glyphOrder = ttFont.getGlyphOrder() diff --git a/Lib/fontTools/ttLib/tables/_h_e_a_d.py b/Lib/fontTools/ttLib/tables/_h_e_a_d.py index fe29c8fc4f..dcadf68a27 100644 --- a/Lib/fontTools/ttLib/tables/_h_e_a_d.py +++ b/Lib/fontTools/ttLib/tables/_h_e_a_d.py @@ -37,6 +37,13 @@ class table__h_e_a_d(DefaultTable.DefaultTable): + """Font Header table + + The ``head`` table contains a variety of font-wide information. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/head + """ + dependencies = ["maxp", "loca", "CFF ", "CFF2"] def decompile(self, data, ttFont): diff --git a/Lib/fontTools/ttLib/tables/_h_h_e_a.py b/Lib/fontTools/ttLib/tables/_h_h_e_a.py index 43e464f74f..1f051e499c 100644 --- a/Lib/fontTools/ttLib/tables/_h_h_e_a.py +++ b/Lib/fontTools/ttLib/tables/_h_h_e_a.py @@ -31,6 +31,18 @@ class table__h_h_e_a(DefaultTable.DefaultTable): + """Horizontal Header table + + The ``hhea`` table contains information needed during horizontal + text layout. + + .. note:: + This converter class is kept in sync with the :class:`._v_h_e_a.table__v_h_e_a` + table constructor. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/hhea + """ + # Note: Keep in sync with table__v_h_e_a dependencies = ["hmtx", "glyf", "CFF ", "CFF2"] diff --git a/Lib/fontTools/ttLib/tables/_h_m_t_x.py b/Lib/fontTools/ttLib/tables/_h_m_t_x.py index 2dbdd7f985..43d49b0925 100644 --- a/Lib/fontTools/ttLib/tables/_h_m_t_x.py +++ b/Lib/fontTools/ttLib/tables/_h_m_t_x.py @@ -12,6 +12,15 @@ class table__h_m_t_x(DefaultTable.DefaultTable): + """Horizontal Metrics table + + The ``hmtx`` table contains per-glyph metrics for the glyphs in a + ``glyf``, ``CFF ``, or ``CFF2`` table, as needed for horizontal text + layout. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx + """ + headerTag = "hhea" advanceName = "width" sideBearingName = "lsb" diff --git a/Lib/fontTools/ttLib/tables/_k_e_r_n.py b/Lib/fontTools/ttLib/tables/_k_e_r_n.py index 270b3b7e44..cdbaebedc6 100644 --- a/Lib/fontTools/ttLib/tables/_k_e_r_n.py +++ b/Lib/fontTools/ttLib/tables/_k_e_r_n.py @@ -12,6 +12,17 @@ class table__k_e_r_n(DefaultTable.DefaultTable): + """Kerning table + + The ``kern`` table contains values that contextually adjust the inter-glyph + spacing for the glyphs in a ``glyf`` table. + + Note that similar contextual spacing adjustments can also be stored + in the "kern" feature of a ``GPOS`` table. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/kern + """ + def getkern(self, format): for subtable in self.kernTables: if subtable.format == format: diff --git a/Lib/fontTools/ttLib/tables/_l_c_a_r.py b/Lib/fontTools/ttLib/tables/_l_c_a_r.py index 1323b670d0..bbb32d23d9 100644 --- a/Lib/fontTools/ttLib/tables/_l_c_a_r.py +++ b/Lib/fontTools/ttLib/tables/_l_c_a_r.py @@ -2,4 +2,12 @@ class table__l_c_a_r(BaseTTXConverter): + """Ligature Caret table + + The AAT ``lcar`` table stores division points within ligatures, which applications + can use to position carets properly between the logical parts of the ligature. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6lcar.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_l_o_c_a.py b/Lib/fontTools/ttLib/tables/_l_o_c_a.py index 39c0c9e39b..713a6eaffc 100644 --- a/Lib/fontTools/ttLib/tables/_l_o_c_a.py +++ b/Lib/fontTools/ttLib/tables/_l_o_c_a.py @@ -8,6 +8,14 @@ class table__l_o_c_a(DefaultTable.DefaultTable): + """Index to Location table + + The ``loca`` table stores the offsets in the ``glyf`` table that correspond + to the descriptions of each glyph. The glyphs are references by Glyph ID. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/loca + """ + dependencies = ["glyf"] def decompile(self, data, ttFont): diff --git a/Lib/fontTools/ttLib/tables/_l_t_a_g.py b/Lib/fontTools/ttLib/tables/_l_t_a_g.py index 24f5e131f0..fc47cd648b 100644 --- a/Lib/fontTools/ttLib/tables/_l_t_a_g.py +++ b/Lib/fontTools/ttLib/tables/_l_t_a_g.py @@ -6,6 +6,14 @@ class table__l_t_a_g(DefaultTable.DefaultTable): + """Language Tag table + + The AAT ``ltag`` table contains mappings between the numeric codes used + in the language field of the ``name`` table and IETF language tags. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6ltag.html + """ + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.version, self.flags = 1, 0 diff --git a/Lib/fontTools/ttLib/tables/_m_a_x_p.py b/Lib/fontTools/ttLib/tables/_m_a_x_p.py index 95b6ab9335..c1576a578f 100644 --- a/Lib/fontTools/ttLib/tables/_m_a_x_p.py +++ b/Lib/fontTools/ttLib/tables/_m_a_x_p.py @@ -27,6 +27,14 @@ class table__m_a_x_p(DefaultTable.DefaultTable): + """Maximum Profile table + + The ``maxp`` table contains the memory requirements for the data in + the font. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/maxp + """ + dependencies = ["glyf"] def decompile(self, data, ttFont): diff --git a/Lib/fontTools/ttLib/tables/_m_e_t_a.py b/Lib/fontTools/ttLib/tables/_m_e_t_a.py index 3af9e54304..c5cea1b993 100644 --- a/Lib/fontTools/ttLib/tables/_m_e_t_a.py +++ b/Lib/fontTools/ttLib/tables/_m_e_t_a.py @@ -24,6 +24,14 @@ class table__m_e_t_a(DefaultTable.DefaultTable): + """Metadata table + + The ``meta`` table contains various metadata values for the font. Each + category of metadata in the table is identified by a four-character tag. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/meta + """ + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.data = {} diff --git a/Lib/fontTools/ttLib/tables/_m_o_r_t.py b/Lib/fontTools/ttLib/tables/_m_o_r_t.py index 261e593e27..5e5fa77ce7 100644 --- a/Lib/fontTools/ttLib/tables/_m_o_r_t.py +++ b/Lib/fontTools/ttLib/tables/_m_o_r_t.py @@ -3,4 +3,12 @@ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html class table__m_o_r_t(BaseTTXConverter): + """The AAT ``mort`` table contains glyph transformations used for script shaping and + for various other optional smart features. + + Note: ``mort`` has been deprecated in favor of the newer ``morx`` table. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6mort.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_m_o_r_x.py b/Lib/fontTools/ttLib/tables/_m_o_r_x.py index da299c6d85..dfb3abcc5e 100644 --- a/Lib/fontTools/ttLib/tables/_m_o_r_x.py +++ b/Lib/fontTools/ttLib/tables/_m_o_r_x.py @@ -3,4 +3,13 @@ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html class table__m_o_r_x(BaseTTXConverter): + """The AAT ``morx`` table contains glyph transformations used for script shaping and + for various other optional smart features, akin to ``GSUB`` and ``GPOS`` features + in OpenType Layout. + + Note: ``morx`` is a replacement for the now deprecated ``mort`` table. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6morx.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_n_a_m_e.py b/Lib/fontTools/ttLib/tables/_n_a_m_e.py index bbb4f5364e..31653f08ec 100644 --- a/Lib/fontTools/ttLib/tables/_n_a_m_e.py +++ b/Lib/fontTools/ttLib/tables/_n_a_m_e.py @@ -36,8 +36,22 @@ class table__n_a_m_e(DefaultTable.DefaultTable): + """Naming table + + The ``name`` table is used to store a variety of strings that can be + associated with user-facing font information. Records in the ``name`` + table can be tagged with language tags to support multilingual naming + and can support platform-specific character-encoding variants. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/name + """ + dependencies = ["ltag"] + def __init__(self, tag=None): + super().__init__(tag) + self.names = [] + def decompile(self, data, ttFont): format, n, stringOffset = struct.unpack(b">HHH", data[:6]) expectedStringOffset = 6 + n * nameRecordSize @@ -68,10 +82,6 @@ def decompile(self, data, ttFont): self.names.append(name) def compile(self, ttFont): - if not hasattr(self, "names"): - # only happens when there are NO name table entries read - # from the TTX file - self.names = [] names = self.names names.sort() # sort according to the spec; see NameRecord.__lt__() stringData = b"" @@ -98,8 +108,6 @@ def toXML(self, writer, ttFont): def fromXML(self, name, attrs, content, ttFont): if name != "namerecord": return # ignore unknown tags - if not hasattr(self, "names"): - self.names = [] name = NameRecord() self.names.append(name) name.fromXML(name, attrs, content, ttFont) @@ -184,8 +192,6 @@ def setName(self, string, nameID, platformID, platEncID, langID): identified by the (platformID, platEncID, langID) triplet. A warning is issued to prevent unexpected results. """ - if not hasattr(self, "names"): - self.names = [] if not isinstance(string, str): if isinstance(string, bytes): log.warning( @@ -252,7 +258,7 @@ def _findUnusedNameID(self, minNameID=256): The nameID is assigned in the range between 'minNameID' and 32767 (inclusive), following the last nameID in the name table. """ - names = getattr(self, "names", []) + names = self.names nameID = 1 + max([n.nameID for n in names] + [minNameID - 1]) if nameID > 32767: raise ValueError("nameID must be less than 32768") @@ -349,8 +355,6 @@ def addMultilingualName( If the 'nameID' argument is None, the created nameID will not be less than the 'minNameID' argument. """ - if not hasattr(self, "names"): - self.names = [] if nameID is None: # Reuse nameID if possible nameID = self.findMultilingualName( @@ -394,8 +398,6 @@ def addName(self, string, platforms=((1, 0, 0), (3, 1, 0x409)), minNameID=255): assert ( len(platforms) > 0 ), "'platforms' must contain at least one (platformID, platEncID, langID) tuple" - if not hasattr(self, "names"): - self.names = [] if not isinstance(string, str): raise TypeError( "expected str, found %s: %r" % (type(string).__name__, string) @@ -1175,17 +1177,8 @@ def __init__(self): @NameRecordVisitor.register_attrs( ( - (otTables.FeatureParamsSize, ("SubfamilyID", "SubfamilyNameID")), + (otTables.FeatureParamsSize, ("SubfamilyNameID",)), (otTables.FeatureParamsStylisticSet, ("UINameID",)), - ( - otTables.FeatureParamsCharacterVariants, - ( - "FeatUILabelNameID", - "FeatUITooltipTextNameID", - "SampleTextNameID", - "FirstParamUILabelNameID", - ), - ), (otTables.STAT, ("ElidedFallbackNameID",)), (otTables.AxisRecord, ("AxisNameID",)), (otTables.AxisValue, ("ValueNameID",)), @@ -1197,6 +1190,22 @@ def visit(visitor, obj, attr, value): visitor.seen.add(value) +@NameRecordVisitor.register(otTables.FeatureParamsCharacterVariants) +def visit(visitor, obj): + for attr in ("FeatUILabelNameID", "FeatUITooltipTextNameID", "SampleTextNameID"): + value = getattr(obj, attr) + visitor.seen.add(value) + # also include the sequence of UI strings for individual variants, if any + if obj.FirstParamUILabelNameID == 0 or obj.NumNamedParameters == 0: + return + visitor.seen.update( + range( + obj.FirstParamUILabelNameID, + obj.FirstParamUILabelNameID + obj.NumNamedParameters, + ) + ) + + @NameRecordVisitor.register(ttLib.getTableClass("fvar")) def visit(visitor, obj): for inst in obj.instances: diff --git a/Lib/fontTools/ttLib/tables/_o_p_b_d.py b/Lib/fontTools/ttLib/tables/_o_p_b_d.py index b22af216bb..b4c5f56829 100644 --- a/Lib/fontTools/ttLib/tables/_o_p_b_d.py +++ b/Lib/fontTools/ttLib/tables/_o_p_b_d.py @@ -3,4 +3,12 @@ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6opbd.html class table__o_p_b_d(BaseTTXConverter): + """Optical Bounds table + + The AAT ``opbd`` table contains optical boundary points for glyphs, which + applications can use for the visual alignment of lines of text. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6opbd.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_p_o_s_t.py b/Lib/fontTools/ttLib/tables/_p_o_s_t.py index dba637117a..c449e5f0c0 100644 --- a/Lib/fontTools/ttLib/tables/_p_o_s_t.py +++ b/Lib/fontTools/ttLib/tables/_p_o_s_t.py @@ -27,6 +27,15 @@ class table__p_o_s_t(DefaultTable.DefaultTable): + """PostScript table + + The ``post`` table contains information needed to use the font on + PostScript printers, including the PostScript names of glyphs and + data that was stored in the ``FontInfo`` dictionary for Type 1 fonts. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/post + """ + def decompile(self, data, ttFont): sstruct.unpack(postFormat, data[:postFormatSize], self) data = data[postFormatSize:] @@ -113,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/_p_r_e_p.py b/Lib/fontTools/ttLib/tables/_p_r_e_p.py index b4b92f3e92..cf4a5dd24f 100644 --- a/Lib/fontTools/ttLib/tables/_p_r_e_p.py +++ b/Lib/fontTools/ttLib/tables/_p_r_e_p.py @@ -4,4 +4,13 @@ class table__p_r_e_p(superclass): + """Control Value Program table + + The ``prep`` table contains TrueType instructions that can makee font-wide + alterations to the Control Value Table. It may potentially be executed + before any glyph is processed. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/prep + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_p_r_o_p.py b/Lib/fontTools/ttLib/tables/_p_r_o_p.py index aead9d7206..1d7de8098d 100644 --- a/Lib/fontTools/ttLib/tables/_p_r_o_p.py +++ b/Lib/fontTools/ttLib/tables/_p_r_o_p.py @@ -3,4 +3,10 @@ # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6prop.html class table__p_r_o_p(BaseTTXConverter): + """The AAT ``prop`` table can store a variety of per-glyph properties, such as + Unicode directionality or whether glyphs are non-spacing marks. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6prop.html + """ + pass diff --git a/Lib/fontTools/ttLib/tables/_s_b_i_x.py b/Lib/fontTools/ttLib/tables/_s_b_i_x.py index 29b82c3e43..a282ea9a60 100644 --- a/Lib/fontTools/ttLib/tables/_s_b_i_x.py +++ b/Lib/fontTools/ttLib/tables/_s_b_i_x.py @@ -28,6 +28,16 @@ class table__s_b_i_x(DefaultTable.DefaultTable): + """Standard Bitmap Graphics table + + The ``sbix`` table stores bitmap image data in standard graphics formats + like JPEG, PNG, or TIFF. The glyphs for which the ``sbix`` table provides + data are indexed by Glyph ID. For each such glyph, the ``sbix`` table can + hold different data for different sizes, called "strikes." + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/sbix + """ + def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) self.version = 1 diff --git a/Lib/fontTools/ttLib/tables/_t_r_a_k.py b/Lib/fontTools/ttLib/tables/_t_r_a_k.py index 0d1b313eae..b0e8e19e08 100644 --- a/Lib/fontTools/ttLib/tables/_t_r_a_k.py +++ b/Lib/fontTools/ttLib/tables/_t_r_a_k.py @@ -58,6 +58,13 @@ class table__t_r_a_k(DefaultTable.DefaultTable): + """The AAT ``trak`` table can store per-size adjustments to each glyph's + sidebearings to make when tracking is enabled, which applications can + use to provide more visually balanced line spacing. + + See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html + """ + dependencies = ["name"] def compile(self, ttFont): diff --git a/Lib/fontTools/ttLib/tables/_v_h_e_a.py b/Lib/fontTools/ttLib/tables/_v_h_e_a.py index de7ce245ad..9c6fa3aefe 100644 --- a/Lib/fontTools/ttLib/tables/_v_h_e_a.py +++ b/Lib/fontTools/ttLib/tables/_v_h_e_a.py @@ -31,6 +31,18 @@ class table__v_h_e_a(DefaultTable.DefaultTable): + """Vertical Header table + + The ``vhea`` table contains information needed during vertical + text layout. + + .. note:: + This converter class is kept in sync with the :class:`._h_h_e_a.table__h_h_e_a` + table constructor. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/vhea + """ + # Note: Keep in sync with table__h_h_e_a dependencies = ["vmtx", "glyf", "CFF ", "CFF2"] diff --git a/Lib/fontTools/ttLib/tables/_v_m_t_x.py b/Lib/fontTools/ttLib/tables/_v_m_t_x.py index a13304c321..32662fc6a2 100644 --- a/Lib/fontTools/ttLib/tables/_v_m_t_x.py +++ b/Lib/fontTools/ttLib/tables/_v_m_t_x.py @@ -4,6 +4,15 @@ class table__v_m_t_x(superclass): + """Vertical Metrics table + + The ``vmtx`` table contains per-glyph metrics for the glyphs in a + ``glyf``, ``CFF ``, or ``CFF2`` table, as needed for vertical text + layout. + + See also https://learn.microsoft.com/en-us/typography/opentype/spec/vmtx + """ + headerTag = "vhea" advanceName = "height" sideBearingName = "tsb" 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/ttLib/tables/otConverters.py b/Lib/fontTools/ttLib/tables/otConverters.py index 656836bd3c..75bfd7f6ca 100644 --- a/Lib/fontTools/ttLib/tables/otConverters.py +++ b/Lib/fontTools/ttLib/tables/otConverters.py @@ -10,7 +10,7 @@ from fontTools.misc.roundTools import nearestMultipleShortestRepr, otRound from fontTools.misc.textTools import bytesjoin, tobytes, tostr, pad, safeEval from fontTools.misc.lazyTools import LazyList -from fontTools.ttLib import getSearchRange +from fontTools.ttLib import OPTIMIZE_FONT_SPEED, getSearchRange from .otBase import ( CountReference, FormatSwitchingBaseTable, @@ -1810,7 +1810,10 @@ def read(self, data, font): return TupleVariation.decompileDeltas_(None, data)[0] def write(self, writer, font, tableDict, values, repeatIndex=None): - return bytes(TupleVariation.compileDeltaValues_(values)) + optimizeSpeed = font.cfg[OPTIMIZE_FONT_SPEED] + return bytes( + TupleVariation.compileDeltaValues_(values, optimizeSize=not optimizeSpeed) + ) def xmlRead(self, attrs, content, font): return safeEval(attrs["value"]) @@ -2016,6 +2019,7 @@ class CompositeMode(_UInt8Enum): # type class "int8": Int8, "int16": Short, + "int32": Long, "uint8": UInt8, "uint16": UShort, "uint24": UInt24, diff --git a/Lib/fontTools/ttLib/tables/otData.py b/Lib/fontTools/ttLib/tables/otData.py index 3a01f5934f..f538aeb48c 100644 --- a/Lib/fontTools/ttLib/tables/otData.py +++ b/Lib/fontTools/ttLib/tables/otData.py @@ -385,7 +385,7 @@ ( "DeltaValue", "DeltaValue", - "", + None, "DeltaFormat in (1,2,3)", "Array of compressed data", ), diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py index bc7fbad915..0a99831906 100644 --- a/Lib/fontTools/ttLib/tables/otTables.py +++ b/Lib/fontTools/ttLib/tables/otTables.py @@ -11,6 +11,7 @@ from math import radians import itertools from collections import defaultdict, namedtuple +from fontTools.ttLib import OPTIMIZE_FONT_SPEED from fontTools.ttLib.tables.TupleVariation import TupleVariation from fontTools.ttLib.tables.otTraverse import dfs_base_table from fontTools.misc.arrayTools import quantizeRect @@ -236,6 +237,8 @@ def read_transform_component(values): return data[i:] def compile(self, font): + optimizeSpeed = font.cfg[OPTIMIZE_FONT_SPEED] + data = [] flags = self.flags @@ -259,7 +262,8 @@ def compile(self, font): data.append(_write_uint32var(self.axisIndicesIndex)) data.append( TupleVariation.compileDeltaValues_( - [fl2fi(v, 14) for v in self.axisValues] + [fl2fi(v, 14) for v in self.axisValues], + optimizeSize=not optimizeSpeed, ) ) else: @@ -2226,24 +2230,28 @@ def computeClipBox( def fixLookupOverFlows(ttf, overflowRecord): """Either the offset from the LookupList to a lookup overflowed, or an offset from a lookup to a subtable overflowed. - The table layout is: - GPSO/GUSB - Script List - Feature List - LookUpList - Lookup[0] and contents - SubTable offset list - SubTable[0] and contents - ... - SubTable[n] and contents - ... - Lookup[n] and contents - SubTable offset list - SubTable[0] and contents - ... - SubTable[n] and contents + + The table layout is:: + + GPSO/GUSB + Script List + Feature List + LookUpList + Lookup[0] and contents + SubTable offset list + SubTable[0] and contents + ... + SubTable[n] and contents + ... + Lookup[n] and contents + SubTable offset list + SubTable[0] and contents + ... + SubTable[n] and contents + If the offset to a lookup overflowed (SubTableIndex is None) - we must promote the *previous* lookup to an Extension type. + we must promote the *previous* lookup to an Extension type. + If the offset from a lookup to subtable overflowed, then we must promote it to an Extension Lookup type. """ diff --git a/Lib/fontTools/ttLib/tables/otTraverse.py b/Lib/fontTools/ttLib/tables/otTraverse.py index ac94218723..9b624533f1 100644 --- a/Lib/fontTools/ttLib/tables/otTraverse.py +++ b/Lib/fontTools/ttLib/tables/otTraverse.py @@ -79,7 +79,8 @@ def bfs_base_table( """Breadth-first search tree of BaseTables. Args: - the root of the tree. + root + the root of the tree. root_accessor (Optional[str]): attribute name for the root table, if any (mostly useful for debugging). skip_root (Optional[bool]): if True, the root itself is not visited, only its diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index f4a539678b..8228ab654e 100644 --- a/Lib/fontTools/ttLib/ttFont.py +++ b/Lib/fontTools/ttLib/ttFont.py @@ -26,37 +26,47 @@ class TTFont(object): accessing tables. Tables will be only decompiled when necessary, ie. when they're actually accessed. This means that simple operations can be extremely fast. - Example usage:: + Example usage: - >> from fontTools import ttLib - >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file - >> tt['maxp'].numGlyphs - 242 - >> tt['OS/2'].achVendID - 'B&H\000' - >> tt['head'].unitsPerEm - 2048 + .. code-block:: pycon - For details of the objects returned when accessing each table, see :ref:`tables`. - To add a table to the font, use the :py:func:`newTable` function:: + >>> + >> from fontTools import ttLib + >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file + >> tt['maxp'].numGlyphs + 242 + >> tt['OS/2'].achVendID + 'B&H\000' + >> tt['head'].unitsPerEm + 2048 - >> os2 = newTable("OS/2") - >> os2.version = 4 - >> # set other attributes - >> font["OS/2"] = os2 + For details of the objects returned when accessing each table, see the + :doc:`tables ` documentation. + To add a table to the font, use the :py:func:`newTable` function: + + .. code-block:: pycon + + >>> + >> os2 = newTable("OS/2") + >> os2.version = 4 + >> # set other attributes + >> font["OS/2"] = os2 TrueType fonts can also be serialized to and from XML format (see also the - :ref:`ttx` binary):: + :doc:`ttx ` binary): + + .. code-block:: pycon - >> tt.saveXML("afont.ttx") - Dumping 'LTSH' table... - Dumping 'OS/2' table... - [...] + >> + >> tt.saveXML("afont.ttx") + Dumping 'LTSH' table... + Dumping 'OS/2' table... + [...] - >> tt2 = ttLib.TTFont() # Create a new font object - >> tt2.importXML("afont.ttx") - >> tt2['maxp'].numGlyphs - 242 + >> tt2 = ttLib.TTFont() # Create a new font object + >> tt2.importXML("afont.ttx") + >> tt2['maxp'].numGlyphs + 242 The TTFont object may be used as a context manager; this will cause the file reader to be closed after the context ``with`` block is exited:: @@ -583,10 +593,8 @@ def _getGlyphNamesFromCmap(self): # temporary cmap and by the real cmap in case we don't find a unicode # cmap. numGlyphs = int(self["maxp"].numGlyphs) - glyphOrder = [None] * numGlyphs + glyphOrder = ["glyph%.5d" % i for i in range(numGlyphs)] glyphOrder[0] = ".notdef" - for i in range(1, numGlyphs): - glyphOrder[i] = "glyph%.5d" % i # Set the glyph order, so the cmap parser has something # to work with (so we don't get called recursively). self.glyphOrder = glyphOrder @@ -596,7 +604,7 @@ def _getGlyphNamesFromCmap(self): # this naming table will usually not cover all glyphs in the font. # If the font has no Unicode cmap table, reversecmap will be empty. if "cmap" in self: - reversecmap = self["cmap"].buildReversed() + reversecmap = self["cmap"].buildReversedMin() else: reversecmap = {} useCount = {} @@ -606,7 +614,7 @@ def _getGlyphNamesFromCmap(self): # If a font maps both U+0041 LATIN CAPITAL LETTER A and # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, # we prefer naming the glyph as "A". - glyphName = self._makeGlyphName(min(reversecmap[tempName])) + glyphName = self._makeGlyphName(reversecmap[tempName]) numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 if numUses > 1: glyphName = "%s.alt%d" % (glyphName, numUses - 1) @@ -981,14 +989,16 @@ def tagToIdentifier(tag): letters get an underscore after the letter. Trailing spaces are trimmed. Illegal characters are escaped as two hex bytes. If the result starts with a number (as the result of a hex escape), an - extra underscore is prepended. Examples:: - - >>> tagToIdentifier('glyf') - '_g_l_y_f' - >>> tagToIdentifier('cvt ') - '_c_v_t' - >>> tagToIdentifier('OS/2') - 'O_S_2f_2' + extra underscore is prepended. Examples: + .. code-block:: pycon + + >>> + >> tagToIdentifier('glyf') + '_g_l_y_f' + >> tagToIdentifier('cvt ') + '_c_v_t' + >> tagToIdentifier('OS/2') + 'O_S_2f_2' """ import re diff --git a/Lib/fontTools/ttLib/ttGlyphSet.py b/Lib/fontTools/ttLib/ttGlyphSet.py index 446c81e7db..3a9a0cb996 100644 --- a/Lib/fontTools/ttLib/ttGlyphSet.py +++ b/Lib/fontTools/ttLib/ttGlyphSet.py @@ -104,34 +104,40 @@ def __getitem__(self, glyphName): return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) -class _TTGlyphSetGlyf(_TTGlyphSet): - def __init__(self, font, location, recalcBounds=True): - self.glyfTable = font["glyf"] - super().__init__(font, location, self.glyfTable, recalcBounds=recalcBounds) - self.gvarTable = font.get("gvar") - - def __getitem__(self, glyphName): - return _TTGlyphGlyf(self, glyphName, recalcBounds=self.recalcBounds) - - class _TTGlyphSetCFF(_TTGlyphSet): def __init__(self, font, location): tableTag = "CFF2" if "CFF2" in font else "CFF " self.charStrings = list(font[tableTag].cff.values())[0].CharStrings super().__init__(font, location, self.charStrings) + self.setLocation(location) + + def __getitem__(self, glyphName): + return _TTGlyphCFF(self, glyphName) + + def setLocation(self, location): self.blender = None if location: + # TODO Optimize by using instancer.setLocation() + from fontTools.varLib.varStore import VarStoreInstancer varStore = getattr(self.charStrings, "varStore", None) if varStore is not None: instancer = VarStoreInstancer( - varStore.otVarStore, font["fvar"].axes, location + varStore.otVarStore, self.font["fvar"].axes, location ) self.blender = instancer.interpolateFromDeltas + else: + self.blender = None - def __getitem__(self, glyphName): - return _TTGlyphCFF(self, glyphName) + @contextmanager + def pushLocation(self, location, reset: bool): + self.setLocation(location) + with _TTGlyphSet.pushLocation(self, location, reset) as value: + try: + yield value + finally: + self.setLocation(self.location) class _TTGlyphSetVARC(_TTGlyphSet): @@ -335,7 +341,6 @@ def _draw(self, pen, isPointPen): ) for comp in glyph.components: - if comp.flags & VarComponentFlags.HAVE_CONDITION: condition = varc.ConditionList.ConditionTable[comp.conditionIndex] if not _evaluateCondition( diff --git a/Lib/fontTools/ttx.py b/Lib/fontTools/ttx.py index 0adda52d74..1a6330cd46 100644 --- a/Lib/fontTools/ttx.py +++ b/Lib/fontTools/ttx.py @@ -101,9 +101,15 @@ --with-zopfli Use Zopfli instead of Zlib to compress WOFF. The Python extension is available at https://pypi.python.org/pypi/zopfli +--optimize-font-speed + Enable optimizations that prioritize speed over file size. + This mainly affects how glyf t able and gvar / VARC tables are + compiled. The produced fonts will be larger, but rendering + performance will be improved with HarfBuzz and other text + layout engines. """ -from fontTools.ttLib import TTFont, TTLibError +from fontTools.ttLib import OPTIMIZE_FONT_SPEED, TTFont, TTLibError from fontTools.misc.macCreatorType import getMacCreatorAndType from fontTools.unicode import setUnicodeData from fontTools.misc.textTools import Tag, tostr @@ -141,6 +147,7 @@ class Options(object): recalcTimestamp = None flavor = None useZopfli = False + optimizeFontSpeed = False def __init__(self, rawOptions, numFiles): self.onlyTables = [] @@ -229,6 +236,8 @@ def __init__(self, rawOptions, numFiles): self.flavor = value elif option == "--with-zopfli": self.useZopfli = True + elif option == "--optimize-font-speed": + self.optimizeFontSpeed = True if self.verbose and self.quiet: raise getopt.GetoptError("-q and -v options are mutually exclusive") if self.verbose: @@ -324,6 +333,8 @@ def ttCompile(input, output, options): recalcBBoxes=options.recalcBBoxes, recalcTimestamp=options.recalcTimestamp, ) + if options.optimizeFontSpeed: + ttf.cfg[OPTIMIZE_FONT_SPEED] = options.optimizeFontSpeed ttf.importXML(input) if options.recalcTimestamp is None and "head" in ttf and input is not sys.stdin: @@ -386,6 +397,7 @@ def parseOptions(args): "version", "with-zopfli", "newline=", + "optimize-font-speed", ], ) diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py index c2d2b0b266..2c5c51d61b 100755 --- a/Lib/fontTools/ufoLib/__init__.py +++ b/Lib/fontTools/ufoLib/__init__.py @@ -1,3 +1,37 @@ +""" +A library for importing .ufo files and their descendants. +Refer to http://unifiedfontobject.org for the UFO specification. + +The main interfaces are the :class:`.UFOReader` and :class:`.UFOWriter` +classes, which support versions 1, 2, and 3 of the UFO specification. + +Set variables are available for external use that list the font +info attribute names for the `fontinfo.plist` formats. These are: + +- :obj:`.fontInfoAttributesVersion1` +- :obj:`.fontInfoAttributesVersion2` +- :obj:`.fontInfoAttributesVersion3` + +A set listing the `fontinfo.plist` attributes that were deprecated +in version 2 is available for external use: + +- :obj:`.deprecatedFontInfoAttributesVersion2` + +Functions that do basic validation on values for `fontinfo.plist` +are available for external use. These are + +- :func:`.validateFontInfoVersion2ValueForAttribute` +- :func:`.validateFontInfoVersion3ValueForAttribute` + +Value conversion functions are available for converting +`fontinfo.plist` values between the possible format versions. + +- :func:`.convertFontInfoValueForAttributeFromVersion1ToVersion2` +- :func:`.convertFontInfoValueForAttributeFromVersion2ToVersion1` +- :func:`.convertFontInfoValueForAttributeFromVersion2ToVersion3` +- :func:`.convertFontInfoValueForAttributeFromVersion3ToVersion2` +""" + import os from copy import deepcopy from os import fsdecode @@ -21,36 +55,6 @@ from fontTools.ufoLib.errors import UFOLibError from fontTools.ufoLib.utils import numberTypes, _VersionTupleEnumMixin -""" -A library for importing .ufo files and their descendants. -Refer to http://unifiedfontobject.com for the UFO specification. - -The UFOReader and UFOWriter classes support versions 1, 2 and 3 -of the specification. - -Sets that list the font info attribute names for the fontinfo.plist -formats are available for external use. These are: - fontInfoAttributesVersion1 - fontInfoAttributesVersion2 - fontInfoAttributesVersion3 - -A set listing the fontinfo.plist attributes that were deprecated -in version 2 is available for external use: - deprecatedFontInfoAttributesVersion2 - -Functions that do basic validation on values for fontinfo.plist -are available for external use. These are - validateFontInfoVersion2ValueForAttribute - validateFontInfoVersion3ValueForAttribute - -Value conversion functions are available for converting -fontinfo.plist values between the possible format versions. - convertFontInfoValueForAttributeFromVersion1ToVersion2 - convertFontInfoValueForAttributeFromVersion2ToVersion1 - convertFontInfoValueForAttributeFromVersion2ToVersion3 - convertFontInfoValueForAttributeFromVersion3ToVersion2 -""" - __all__ = [ "makeUFOPath", "UFOLibError", @@ -197,8 +201,12 @@ def _writePlist(self, fileName, obj): class UFOReader(_UFOBaseIO): - """ - Read the various components of the .ufo. + """Read the various components of a .ufo. + + Attributes: + path: An :class:`os.PathLike` object pointing to the .ufo. + validate: A boolean indicating if the data read should be + validated. Defaults to `True`. By default read data is validated. Set ``validate`` to ``False`` to not validate the data. @@ -646,7 +654,7 @@ def readFeatures(self): The returned string is empty if the file is missing. """ try: - with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8") as f: + with self.fs.open(FEATURES_FILENAME, "r", encoding="utf-8-sig") as f: return f.read() except fs.errors.ResourceNotFound: return "" @@ -880,20 +888,27 @@ def __exit__(self, exc_type, exc_value, exc_tb): class UFOWriter(UFOReader): - """ - Write the various components of the .ufo. + """Write the various components of a .ufo. + + Attributes: + 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, + which is equivalent to formatVersion=(3, 0). + fileCreator: The creator of the .ufo file. Defaults to + `com.github.fonttools.ufoLib`. + structure: The internal structure of the .ufo file: either `ZIP` or `PACKAGE`. + validate: A boolean indicating if the data read should be validated. Defaults + to `True`. By default, the written data will be validated before writing. Set ``validate`` to ``False`` if you do not want to validate the data. Validation can also be overriden - on a per method level if desired. - - The ``formatVersion`` argument allows to specify the UFO format version as a tuple - of integers (major, minor), or as a single integer for the major digit only (minor - is implied as 0). By default the latest formatVersion will be used; currently it's - 3.0, which is equivalent to formatVersion=(3, 0). + on a per-method level if desired. - An UnsupportedUFOFormat exception is raised if the requested UFO formatVersion is - not supported. + Raises: + UnsupportedUFOFormat: An exception indicating that the requested UFO + formatVersion is not supported. """ def __init__( 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 62e87db0df..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) @@ -1191,8 +1198,12 @@ def _readGlyphFromTreeFormat1( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": + v = element.get("hex") + if v is None: + raise GlifLibError( + "A unicode element is missing its required hex attribute." + ) try: - v = element.get("hex") v = int(v, 16) if v not in unicodes: unicodes.append(v) @@ -1254,8 +1265,12 @@ def _readGlyphFromTreeFormat2( haveSeenAdvance = True _readAdvance(glyphObject, element) elif element.tag == "unicode": + v = element.get("hex") + if v is None: + raise GlifLibError( + "A unicode element is missing its required hex attribute." + ) try: - v = element.get("hex") v = int(v, 16) if v not in unicodes: unicodes.append(v) @@ -1757,7 +1772,7 @@ def parse(self, text): parser = ParserCreate() parser.StartElementHandler = self.startElementHandler parser.EndElementHandler = self.endElementHandler - parser.Parse(text) + parser.Parse(text, 1) def startElementHandler(self, name, attrs): self._elementStack.append(name) 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/plistlib.py b/Lib/fontTools/ufoLib/plistlib.py index 38bb266b21..0242e9d26f 100644 --- a/Lib/fontTools/ufoLib/plistlib.py +++ b/Lib/fontTools/ufoLib/plistlib.py @@ -1,5 +1,5 @@ """DEPRECATED - This module is kept here only as a backward compatibility shim -for the old ufoLib.plistlib module, which was moved to fontTools.misc.plistlib. +for the old `ufoLib.plistlib` module, which was moved to :class:`fontTools.misc.plistlib`. Please use the latter instead. """ diff --git a/Lib/fontTools/ufoLib/pointPen.py b/Lib/fontTools/ufoLib/pointPen.py index baef9a583e..4a8126cd64 100644 --- a/Lib/fontTools/ufoLib/pointPen.py +++ b/Lib/fontTools/ufoLib/pointPen.py @@ -1,5 +1,5 @@ """DEPRECATED - This module is kept here only as a backward compatibility shim -for the old ufoLib.pointPen module, which was moved to fontTools.pens.pointPen. +for the old `ufoLib.pointPen` module, which was moved to :class:`fontTools.pens.pointPen`. Please use the latter instead. """ 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/Blocks.py b/Lib/fontTools/unicodedata/Blocks.py index b35c93d9b6..c0e771842b 100644 --- a/Lib/fontTools/unicodedata/Blocks.py +++ b/Lib/fontTools/unicodedata/Blocks.py @@ -4,10 +4,11 @@ # Source: https://unicode.org/Public/UNIDATA/Blocks.txt # License: http://unicode.org/copyright.html#License # -# Blocks-15.0.0.txt -# Date: 2022-01-28, 20:58:00 GMT [KW] -# © 2022 Unicode®, Inc. -# For terms of use, see https://www.unicode.org/terms_of_use.html +# Blocks-16.0.0.txt +# Date: 2024-02-02 +# © 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/ @@ -15,7 +16,6 @@ # Format: # Start Code..End Code; Block Name - RANGES = [ 0x0000, # .. 0x007F ; Basic Latin 0x0080, # .. 0x00FF ; Latin-1 Supplement @@ -205,7 +205,7 @@ 0x10500, # .. 0x1052F ; Elbasan 0x10530, # .. 0x1056F ; Caucasian Albanian 0x10570, # .. 0x105BF ; Vithkuqi - 0x105C0, # .. 0x105FF ; No_Block + 0x105C0, # .. 0x105FF ; Todhri 0x10600, # .. 0x1077F ; Linear A 0x10780, # .. 0x107BF ; Latin Extended-F 0x107C0, # .. 0x107FF ; No_Block @@ -234,7 +234,8 @@ 0x10C50, # .. 0x10C7F ; No_Block 0x10C80, # .. 0x10CFF ; Old Hungarian 0x10D00, # .. 0x10D3F ; Hanifi Rohingya - 0x10D40, # .. 0x10E5F ; No_Block + 0x10D40, # .. 0x10D8F ; Garay + 0x10D90, # .. 0x10E5F ; No_Block 0x10E60, # .. 0x10E7F ; Rumi Numeral Symbols 0x10E80, # .. 0x10EBF ; Yezidi 0x10EC0, # .. 0x10EFF ; Arabic Extended-C @@ -255,7 +256,7 @@ 0x11280, # .. 0x112AF ; Multani 0x112B0, # .. 0x112FF ; Khudawadi 0x11300, # .. 0x1137F ; Grantha - 0x11380, # .. 0x113FF ; No_Block + 0x11380, # .. 0x113FF ; Tulu-Tigalari 0x11400, # .. 0x1147F ; Newa 0x11480, # .. 0x114DF ; Tirhuta 0x114E0, # .. 0x1157F ; No_Block @@ -263,7 +264,7 @@ 0x11600, # .. 0x1165F ; Modi 0x11660, # .. 0x1167F ; Mongolian Supplement 0x11680, # .. 0x116CF ; Takri - 0x116D0, # .. 0x116FF ; No_Block + 0x116D0, # .. 0x116FF ; Myanmar Extended-C 0x11700, # .. 0x1174F ; Ahom 0x11750, # .. 0x117FF ; No_Block 0x11800, # .. 0x1184F ; Dogra @@ -277,7 +278,8 @@ 0x11AB0, # .. 0x11ABF ; Unified Canadian Aboriginal Syllabics Extended-A 0x11AC0, # .. 0x11AFF ; Pau Cin Hau 0x11B00, # .. 0x11B5F ; Devanagari Extended-A - 0x11B60, # .. 0x11BFF ; No_Block + 0x11B60, # .. 0x11BBF ; No_Block + 0x11BC0, # .. 0x11BFF ; Sunuwar 0x11C00, # .. 0x11C6F ; Bhaiksuki 0x11C70, # .. 0x11CBF ; Marchen 0x11CC0, # .. 0x11CFF ; No_Block @@ -296,15 +298,19 @@ 0x12F90, # .. 0x12FFF ; Cypro-Minoan 0x13000, # .. 0x1342F ; Egyptian Hieroglyphs 0x13430, # .. 0x1345F ; Egyptian Hieroglyph Format Controls - 0x13460, # .. 0x143FF ; No_Block + 0x13460, # .. 0x143FF ; Egyptian Hieroglyphs Extended-A 0x14400, # .. 0x1467F ; Anatolian Hieroglyphs - 0x14680, # .. 0x167FF ; No_Block + 0x14680, # .. 0x160FF ; No_Block + 0x16100, # .. 0x1613F ; Gurung Khema + 0x16140, # .. 0x167FF ; No_Block 0x16800, # .. 0x16A3F ; Bamum Supplement 0x16A40, # .. 0x16A6F ; Mro 0x16A70, # .. 0x16ACF ; Tangsa 0x16AD0, # .. 0x16AFF ; Bassa Vah 0x16B00, # .. 0x16B8F ; Pahawh Hmong - 0x16B90, # .. 0x16E3F ; No_Block + 0x16B90, # .. 0x16D3F ; No_Block + 0x16D40, # .. 0x16D7F ; Kirat Rai + 0x16D80, # .. 0x16E3F ; No_Block 0x16E40, # .. 0x16E9F ; Medefaidrin 0x16EA0, # .. 0x16EFF ; No_Block 0x16F00, # .. 0x16F9F ; Miao @@ -323,7 +329,9 @@ 0x1B300, # .. 0x1BBFF ; No_Block 0x1BC00, # .. 0x1BC9F ; Duployan 0x1BCA0, # .. 0x1BCAF ; Shorthand Format Controls - 0x1BCB0, # .. 0x1CEFF ; No_Block + 0x1BCB0, # .. 0x1CBFF ; No_Block + 0x1CC00, # .. 0x1CEBF ; Symbols for Legacy Computing Supplement + 0x1CEC0, # .. 0x1CEFF ; No_Block 0x1CF00, # .. 0x1CFCF ; Znamenny Musical Notation 0x1CFD0, # .. 0x1CFFF ; No_Block 0x1D000, # .. 0x1D0FF ; Byzantine Musical Symbols @@ -348,7 +356,9 @@ 0x1E2C0, # .. 0x1E2FF ; Wancho 0x1E300, # .. 0x1E4CF ; No_Block 0x1E4D0, # .. 0x1E4FF ; Nag Mundari - 0x1E500, # .. 0x1E7DF ; No_Block + 0x1E500, # .. 0x1E5CF ; No_Block + 0x1E5D0, # .. 0x1E5FF ; Ol Onal + 0x1E600, # .. 0x1E7DF ; No_Block 0x1E7E0, # .. 0x1E7FF ; Ethiopic Extended-B 0x1E800, # .. 0x1E8DF ; Mende Kikakui 0x1E8E0, # .. 0x1E8FF ; No_Block @@ -383,7 +393,8 @@ 0x2B740, # .. 0x2B81F ; CJK Unified Ideographs Extension D 0x2B820, # .. 0x2CEAF ; CJK Unified Ideographs Extension E 0x2CEB0, # .. 0x2EBEF ; CJK Unified Ideographs Extension F - 0x2EBF0, # .. 0x2F7FF ; No_Block + 0x2EBF0, # .. 0x2EE5F ; CJK Unified Ideographs Extension I + 0x2EE60, # .. 0x2F7FF ; No_Block 0x2F800, # .. 0x2FA1F ; CJK Compatibility Ideographs Supplement 0x2FA20, # .. 0x2FFFF ; No_Block 0x30000, # .. 0x3134F ; CJK Unified Ideographs Extension G @@ -586,7 +597,7 @@ "Elbasan", # 10500..1052F "Caucasian Albanian", # 10530..1056F "Vithkuqi", # 10570..105BF - "No_Block", # 105C0..105FF + "Todhri", # 105C0..105FF "Linear A", # 10600..1077F "Latin Extended-F", # 10780..107BF "No_Block", # 107C0..107FF @@ -615,7 +626,8 @@ "No_Block", # 10C50..10C7F "Old Hungarian", # 10C80..10CFF "Hanifi Rohingya", # 10D00..10D3F - "No_Block", # 10D40..10E5F + "Garay", # 10D40..10D8F + "No_Block", # 10D90..10E5F "Rumi Numeral Symbols", # 10E60..10E7F "Yezidi", # 10E80..10EBF "Arabic Extended-C", # 10EC0..10EFF @@ -636,7 +648,7 @@ "Multani", # 11280..112AF "Khudawadi", # 112B0..112FF "Grantha", # 11300..1137F - "No_Block", # 11380..113FF + "Tulu-Tigalari", # 11380..113FF "Newa", # 11400..1147F "Tirhuta", # 11480..114DF "No_Block", # 114E0..1157F @@ -644,7 +656,7 @@ "Modi", # 11600..1165F "Mongolian Supplement", # 11660..1167F "Takri", # 11680..116CF - "No_Block", # 116D0..116FF + "Myanmar Extended-C", # 116D0..116FF "Ahom", # 11700..1174F "No_Block", # 11750..117FF "Dogra", # 11800..1184F @@ -658,7 +670,8 @@ "Unified Canadian Aboriginal Syllabics Extended-A", # 11AB0..11ABF "Pau Cin Hau", # 11AC0..11AFF "Devanagari Extended-A", # 11B00..11B5F - "No_Block", # 11B60..11BFF + "No_Block", # 11B60..11BBF + "Sunuwar", # 11BC0..11BFF "Bhaiksuki", # 11C00..11C6F "Marchen", # 11C70..11CBF "No_Block", # 11CC0..11CFF @@ -677,15 +690,19 @@ "Cypro-Minoan", # 12F90..12FFF "Egyptian Hieroglyphs", # 13000..1342F "Egyptian Hieroglyph Format Controls", # 13430..1345F - "No_Block", # 13460..143FF + "Egyptian Hieroglyphs Extended-A", # 13460..143FF "Anatolian Hieroglyphs", # 14400..1467F - "No_Block", # 14680..167FF + "No_Block", # 14680..160FF + "Gurung Khema", # 16100..1613F + "No_Block", # 16140..167FF "Bamum Supplement", # 16800..16A3F "Mro", # 16A40..16A6F "Tangsa", # 16A70..16ACF "Bassa Vah", # 16AD0..16AFF "Pahawh Hmong", # 16B00..16B8F - "No_Block", # 16B90..16E3F + "No_Block", # 16B90..16D3F + "Kirat Rai", # 16D40..16D7F + "No_Block", # 16D80..16E3F "Medefaidrin", # 16E40..16E9F "No_Block", # 16EA0..16EFF "Miao", # 16F00..16F9F @@ -704,7 +721,9 @@ "No_Block", # 1B300..1BBFF "Duployan", # 1BC00..1BC9F "Shorthand Format Controls", # 1BCA0..1BCAF - "No_Block", # 1BCB0..1CEFF + "No_Block", # 1BCB0..1CBFF + "Symbols for Legacy Computing Supplement", # 1CC00..1CEBF + "No_Block", # 1CEC0..1CEFF "Znamenny Musical Notation", # 1CF00..1CFCF "No_Block", # 1CFD0..1CFFF "Byzantine Musical Symbols", # 1D000..1D0FF @@ -729,7 +748,9 @@ "Wancho", # 1E2C0..1E2FF "No_Block", # 1E300..1E4CF "Nag Mundari", # 1E4D0..1E4FF - "No_Block", # 1E500..1E7DF + "No_Block", # 1E500..1E5CF + "Ol Onal", # 1E5D0..1E5FF + "No_Block", # 1E600..1E7DF "Ethiopic Extended-B", # 1E7E0..1E7FF "Mende Kikakui", # 1E800..1E8DF "No_Block", # 1E8E0..1E8FF @@ -764,7 +785,8 @@ "CJK Unified Ideographs Extension D", # 2B740..2B81F "CJK Unified Ideographs Extension E", # 2B820..2CEAF "CJK Unified Ideographs Extension F", # 2CEB0..2EBEF - "No_Block", # 2EBF0..2F7FF + "CJK Unified Ideographs Extension I", # 2EBF0..2EE5F + "No_Block", # 2EE60..2F7FF "CJK Compatibility Ideographs Supplement", # 2F800..2FA1F "No_Block", # 2FA20..2FFFF "CJK Unified Ideographs Extension G", # 30000..3134F 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/ScriptExtensions.py b/Lib/fontTools/unicodedata/ScriptExtensions.py index 2ecc5daed8..d6fe68fd2c 100644 --- a/Lib/fontTools/unicodedata/ScriptExtensions.py +++ b/Lib/fontTools/unicodedata/ScriptExtensions.py @@ -4,11 +4,11 @@ # Source: https://unicode.org/Public/UNIDATA/ScriptExtensions.txt # License: http://unicode.org/copyright.html#License # -# ScriptExtensions-15.0.0.txt -# Date: 2022-02-02, 00:57:11 GMT -# © 2022 Unicode®, Inc. +# ScriptExtensions-16.0.0.txt +# Date: 2024-07-30, 19:38:00 GMT +# © 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, see https://www.unicode.org/terms_of_use.html +# 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/ @@ -27,38 +27,85 @@ # values in that set is not material, but for stability in presentation # it is given here as alphabetical. # -# The Script_Extensions values are presented in sorted order in the file. -# They are sorted first by the number of Script property values in their sets, -# and then alphabetically by first differing Script property value. -# -# Following each distinct Script_Extensions value is the list of code -# points associated with that value, listed in code point order. -# # All code points not explicitly listed for Script_Extensions -# have as their value the corresponding Script property value +# have as their value the corresponding Script property value. # # @missing: 0000..10FFFF; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/contextual_inline_format_4.fea b/Tests/feaLib/data/contextual_inline_format_4.fea new file mode 100644 index 0000000000..6c416b3dee --- /dev/null +++ b/Tests/feaLib/data/contextual_inline_format_4.fea @@ -0,0 +1,6 @@ +feature test { + # we should only produce two lookups: one contextual, and one ligature, + # with the ligature containing `l o -> z` and `L o` -> Z + sub l' o' l by z; + sub L' o' L by Z; +} test; diff --git a/Tests/feaLib/data/contextual_inline_format_4.ttx b/Tests/feaLib/data/contextual_inline_format_4.ttx new file mode 100644 index 0000000000..0769afcbd6 --- /dev/null +++ b/Tests/feaLib/data/contextual_inline_format_4.ttx @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/contextual_inline_multi_sub_format_2.fea b/Tests/feaLib/data/contextual_inline_multi_sub_format_2.fea new file mode 100644 index 0000000000..9a9bfe20de --- /dev/null +++ b/Tests/feaLib/data/contextual_inline_multi_sub_format_2.fea @@ -0,0 +1,17 @@ +# reduced from the ccmp feature in Oswald +feature ccmp { + lookup ccmp_Other_1 { + @CombiningTopAccents = [acutecomb brevecomb]; + @CombiningNonTopAccents = [dotbelowcomb ogonekcomb]; + lookupflag UseMarkFilteringSet @CombiningTopAccents; + # we should only generate two lookups; one contextual and one multiple sub, + # containing 'sub idotbelow by idotless dotbelowcomb' and + # 'sub iogonek by idotless ogonekcomb' + sub idotbelow' @CombiningTopAccents by idotless dotbelowcomb; + sub iogonek' @CombiningTopAccents by idotless ogonekcomb; + sub idotbelow' @CombiningNonTopAccents @CombiningTopAccents by idotless dotbelowcomb; + sub iogonek' @CombiningNonTopAccents @CombiningTopAccents by idotless ogonekcomb; + } ccmp_Other_1; +} ccmp; + + diff --git a/Tests/feaLib/data/contextual_inline_multi_sub_format_2.ttx b/Tests/feaLib/data/contextual_inline_multi_sub_format_2.ttx new file mode 100644 index 0000000000..d09431fa49 --- /dev/null +++ b/Tests/feaLib/data/contextual_inline_multi_sub_format_2.ttx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/duplicate_language_stmt.fea b/Tests/feaLib/data/duplicate_language_stmt.fea new file mode 100644 index 0000000000..b74e772316 --- /dev/null +++ b/Tests/feaLib/data/duplicate_language_stmt.fea @@ -0,0 +1,19 @@ +languagesystem DFLT dflt; +languagesystem latn NLD; + +feature locl { + + script latn; + language NLD; + lookup one { + sub a by b; + } one; + + # if we encounter a duplicate script/lang statement we want to combine them + # (lookups https://github.com/fonttools/fonttools/issues/3748) + script latn; + language NLD; + lookup two { + sub x by z; + } two; +} locl; diff --git a/Tests/feaLib/data/duplicate_language_stmt.ttx b/Tests/feaLib/data/duplicate_language_stmt.ttx new file mode 100644 index 0000000000..8e86caf2d7 --- /dev/null +++ b/Tests/feaLib/data/duplicate_language_stmt.ttx @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/spec9a2.fea b/Tests/feaLib/data/spec9a2.fea new file mode 100644 index 0000000000..425bdb0a97 --- /dev/null +++ b/Tests/feaLib/data/spec9a2.fea @@ -0,0 +1,7 @@ +table BASE { + HorizAxis.BaseTagList ideo romn; + HorizAxis.BaseScriptList latn romn -120 0, cyrl romn -120 0, grek romn -120 0, hani ideo -120 0, kana ideo -120 0, hang ideo -120 0; + HorizAxis.MinMax cyrl dflt -800, 1200; + # Some comment + HorizAxis.MinMax cyrl RUS -1000, 1000; +} BASE; diff --git a/Tests/feaLib/data/spec9a2.ttx b/Tests/feaLib/data/spec9a2.ttx new file mode 100644 index 0000000000..3900b12f58 --- /dev/null +++ b/Tests/feaLib/data/spec9a2.ttx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/spec9c1.ttx b/Tests/feaLib/data/spec9c1.ttx index b60e0f5235..e6ba4d3e61 100644 --- a/Tests/feaLib/data/spec9c1.ttx +++ b/Tests/feaLib/data/spec9c1.ttx @@ -6,7 +6,7 @@ - + diff --git a/Tests/feaLib/data/spec9c2.ttx b/Tests/feaLib/data/spec9c2.ttx index ed3d74024a..709e1c2545 100644 --- a/Tests/feaLib/data/spec9c2.ttx +++ b/Tests/feaLib/data/spec9c2.ttx @@ -6,7 +6,7 @@ - + diff --git a/Tests/feaLib/data/spec9c3.ttx b/Tests/feaLib/data/spec9c3.ttx index e20186cfcd..01dfa81ab1 100644 --- a/Tests/feaLib/data/spec9c3.ttx +++ b/Tests/feaLib/data/spec9c3.ttx @@ -6,7 +6,7 @@ - + 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/data/variable_mark_anchor.ttx b/Tests/feaLib/data/variable_mark_anchor.ttx index 962cff7411..d29fc43a1d 100644 --- a/Tests/feaLib/data/variable_mark_anchor.ttx +++ b/Tests/feaLib/data/variable_mark_anchor.ttx @@ -16,7 +16,7 @@ - + diff --git a/Tests/feaLib/data/variable_scalar_anchor.ttx b/Tests/feaLib/data/variable_scalar_anchor.ttx index 6bb55691f6..92a456d3ba 100644 --- a/Tests/feaLib/data/variable_scalar_anchor.ttx +++ b/Tests/feaLib/data/variable_scalar_anchor.ttx @@ -12,7 +12,7 @@ - + @@ -24,12 +24,12 @@ - + - + diff --git a/Tests/feaLib/data/variable_scalar_valuerecord.ttx b/Tests/feaLib/data/variable_scalar_valuerecord.ttx index e3251f6914..94bd3867d6 100644 --- a/Tests/feaLib/data/variable_scalar_valuerecord.ttx +++ b/Tests/feaLib/data/variable_scalar_valuerecord.ttx @@ -12,7 +12,7 @@ - + @@ -24,12 +24,12 @@ - + - + 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/fontBuilder/data/test_var.ttf.ttx b/Tests/fontBuilder/data/test_var.ttf.ttx index e6d4d8d549..132e80d73f 100644 --- a/Tests/fontBuilder/data/test_var.ttf.ttx +++ b/Tests/fontBuilder/data/test_var.ttf.ttx @@ -177,39 +177,6 @@ - - HelloTestFont - - - TotallyNormal - - - HelloTestFont-TotallyNormal - - - Left - - - Right - - - Up - - - Down - - - Right Up - - - Neutral - - - HalloTestFont - - - TotaalNormaal - HelloTestFont diff --git a/Tests/fontBuilder/fontBuilder_test.py b/Tests/fontBuilder/fontBuilder_test.py index 0cbf5d0cef..0138dcd008 100644 --- a/Tests/fontBuilder/fontBuilder_test.py +++ b/Tests/fontBuilder/fontBuilder_test.py @@ -236,7 +236,7 @@ def test_build_var(tmpdir): fb.setupHorizontalMetrics(metrics) fb.setupHorizontalHeader(ascent=824, descent=200) - fb.setupNameTable(nameStrings) + fb.setupNameTable(nameStrings, mac=False) axes = [ ("LEFT", 0, 0, 100, "Left"), diff --git a/Tests/merge/merge_test.py b/Tests/merge/merge_test.py index 5558a2e381..6bb93bbf62 100644 --- a/Tests/merge/merge_test.py +++ b/Tests/merge/merge_test.py @@ -139,6 +139,11 @@ def makeSubtable(self, format, platformID, platEncID, cmap): ) return subtable + def makeUvsSubtable(self, format, platformID, platEncID, uvsDict): + subtable = self.makeSubtable(format, platformID, platEncID, {}) + subtable.uvsDict = uvsDict + return subtable + # 4-3-1 table merged with 12-3-10 table with no dupes with codepoints outside BMP def test_cmap_merge_no_dupes(self): table1 = self.table1 @@ -200,6 +205,117 @@ def test_cmap_merge_three_dupes(self): self.merger.duplicateGlyphsPerFont, [{}, {"space#0": "space#1"}] ) + def test_cmap_merge_format_14(self): + table1 = self.table1 + table2 = self.table2 + mergedTable = self.mergedTable + + cmap1 = {0x3404: "uni3404"} + uvsDict1 = {0xFE00: [(0x0030, "uni0030.FE00")], 0xE0101: [(0x3404, "uni3404")]} + table1.tables = [ + self.makeUvsSubtable(14, 0, 5, uvsDict1), + self.makeSubtable(4, 3, 1, cmap1), + ] + uvsDict2 = {0xFE00: [(0x20122, "u2F803")], 0xE0102: [(0x34FF, "uni34FF.var2")]} + table2.tables = [self.makeUvsSubtable(14, 0, 5, uvsDict2)] + + self.merger.duplicateGlyphsPerFont = [{}, {}] + mergedTable.merge(self.merger, [table1, table2]) + + self.assertEqual(mergedTable.numSubTables, 2) + (uvsTable, cmapTable) = mergedTable.tables + + self.assertEqual( + ( + uvsTable.format, + uvsTable.platformID, + uvsTable.platEncID, + uvsTable.language, + ), + (14, 0, 5, 0), + ) + self.assertEqual( + ( + cmapTable.format, + cmapTable.platformID, + cmapTable.platEncID, + cmapTable.language, + ), + (4, 3, 1, 0), + ) + + expectedUvsDict = { + 0xFE00: [(0x0030, "uni0030.FE00"), (0x20122, "u2F803")], + 0xE0101: [(0x3404, None)], + 0xE0102: [(0x34FF, "uni34FF.var2")], + } + self.assertEqual(uvsTable.uvsDict, expectedUvsDict) + + def test_cmap_merge_format_14_dupes(self): + table1 = self.table1 + table2 = self.table2 + mergedTable = self.mergedTable + + cmap1 = {0x2F803: "u2F803#0"} + uvsDict1 = {0xFE00: [(0x20122, "u2F803#0")], 0xE0101: [(0x3404, "uni3404")]} + table1.tables = [ + self.makeUvsSubtable(14, 0, 5, uvsDict1), + self.makeSubtable(12, 3, 10, cmap1), + ] + cmap2 = {0x2F803: "u2F803#1"} + uvsDict2 = { + 0xFE00: [(0x20122, "u2F803#1")], + 0xE0102: [(0x34FF, "uni34FF.var2")], + } + table2.tables = [ + self.makeUvsSubtable(14, 0, 5, uvsDict2), + self.makeSubtable(12, 3, 10, cmap2), + ] + + self.merger.duplicateGlyphsPerFont = [{}, {}] + mergedTable.merge(self.merger, [table1, table2]) + + self.assertEqual(mergedTable.numSubTables, 3) + (uvsTable, cmap_4_3_1_Table, cmap_12_3_10_Table) = mergedTable.tables + + self.assertEqual( + ( + uvsTable.format, + uvsTable.platformID, + uvsTable.platEncID, + uvsTable.language, + ), + (14, 0, 5, 0), + ) + self.assertEqual( + ( + cmap_4_3_1_Table.format, + cmap_4_3_1_Table.platformID, + cmap_4_3_1_Table.platEncID, + cmap_4_3_1_Table.language, + ), + (4, 3, 1, 0), + ) + self.assertEqual( + ( + cmap_12_3_10_Table.format, + cmap_12_3_10_Table.platformID, + cmap_12_3_10_Table.platEncID, + cmap_12_3_10_Table.language, + ), + (12, 3, 10, 0), + ) + + expectedUvsDict = { + 0xFE00: [(0x20122, "u2F803#0")], + 0xE0101: [(0x3404, "uni3404")], + 0xE0102: [(0x34FF, "uni34FF.var2")], + } + self.assertEqual(uvsTable.uvsDict, expectedUvsDict) + self.assertEqual( + self.merger.duplicateGlyphsPerFont, [{}, {"u2F803#0": "u2F803#1"}] + ) + def _compile(ttFont): buf = io.BytesIO() diff --git a/Tests/misc/bezierTools_test.py b/Tests/misc/bezierTools_test.py index 6107a39dfb..fe7a3d2142 100644 --- a/Tests/misc/bezierTools_test.py +++ b/Tests/misc/bezierTools_test.py @@ -144,6 +144,12 @@ def test_splitCubicAtT(): ] +def test_splitCubicAtT_robustness(): + segment = ((-103, -231), (-61, -240), (-31.009, -245), (6, -245)) + (_, tail) = splitCubicAtT(*segment, 0.386637) + assert tail[-1] == segment[-1] + + def test_solveCubic(): assert solveCubic(1, 1, -6, 0) == [-3.0, -0.0, 2.0] assert solveCubic(-10.0, -9.0, 48.0, -29.0) == [-2.9, 1.0, 1.0] 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/misc/transform_test.py b/Tests/misc/transform_test.py index eaa1667846..049195eb59 100644 --- a/Tests/misc/transform_test.py +++ b/Tests/misc/transform_test.py @@ -123,7 +123,6 @@ def test_decompose(self): assert d.translateX == 5 assert d.translateY == 7 - def test_decompose(self): t = Transform(-1, 0, 0, 1, 0, 0) d = t.toDecomposed() assert d.scaleX == -1 diff --git a/Tests/misc/visitor_test.py b/Tests/misc/visitor_test.py index 268cc716d7..eb1969ea86 100644 --- a/Tests/misc/visitor_test.py +++ b/Tests/misc/visitor_test.py @@ -24,6 +24,10 @@ def __init__(self): self.a = A() +class C(B): + pass + + class TestVisitor(Visitor): def __init__(self): self.value = [] @@ -71,3 +75,9 @@ def test_visitor(self): visitor.defaultStop = True visitor.visit(b) assert visitor.value == ["B", "B a"] + + def test_visitor_inheritance(self): + b = C() # Should behave just like a B() + visitor = TestVisitor() + visitor.visit(b) + assert visitor.value == ["B", "B a", "A", 1, 2, 3, 5, 7, "e", E.E2, 10] diff --git a/Tests/otlLib/maxContextCalc_test.py b/Tests/otlLib/maxContextCalc_test.py index f672052eee..a94ba246e5 100644 --- a/Tests/otlLib/maxContextCalc_test.py +++ b/Tests/otlLib/maxContextCalc_test.py @@ -39,8 +39,8 @@ def test_max_ctx_calc_features(): rsub a' by b; rsub a b' by c; rsub a b' c by A; + rsub [a b] [a b c]' [a b] by B; rsub [a b] c' by A; - rsub [a b] c' [a b] by B; lookup GSUB_EXT; } sub1; diff --git a/Tests/otlLib/optimize_test.py b/Tests/otlLib/optimize_test.py index a2e4332253..15df547470 100644 --- a/Tests/otlLib/optimize_test.py +++ b/Tests/otlLib/optimize_test.py @@ -2,7 +2,6 @@ import logging import os from pathlib import Path -from subprocess import run from typing import List, Optional, Tuple import pytest @@ -28,18 +27,48 @@ def test_main(tmpdir: Path): input = tmpdir / "in.ttf" fb.save(str(input)) output = tmpdir / "out.ttf" - run( - [ - "fonttools", - "otlLib.optimize", - "--gpos-compression-level", - "5", - str(input), - "-o", - str(output), - ], - check=True, - ) + args = [ + "--gpos-compression-level", + "5", + str(input), + "-o", + str(output), + ] + from fontTools.otlLib.optimize import main + + ret = main(args) + assert ret in (0, None) + assert output.exists() + + +def test_no_crash_with_missing_gpos(tmpdir: Path): + """Test that the optimize script gracefully handles TTFs with no GPOS.""" + + # Create a test TTF. + glyphs = ".notdef space A Aacute B D".split() + fb = FontBuilder(1000) + fb.setupGlyphOrder(glyphs) + + # Confirm that it has no GPOS. + assert "GPOS" not in fb.font + + # Save, and feed to the optimize CLI. + input = tmpdir / "in.ttf" + fb.save(str(input)) + + output = tmpdir / "out.ttf" + args = [ + "--gpos-compression-level", + "5", + str(input), + "-o", + str(output), + ] + from fontTools.otlLib.optimize import main + + # Assert that we did not crash, and saved an output font. + ret = main(args) + assert ret in (0, None) assert output.exists() diff --git a/Tests/pens/ttGlyphPen_test.py b/Tests/pens/ttGlyphPen_test.py index 6a74cb25a4..4cc43bd006 100644 --- a/Tests/pens/ttGlyphPen_test.py +++ b/Tests/pens/ttGlyphPen_test.py @@ -3,6 +3,7 @@ import struct from fontTools import ttLib +from fontTools.misc.roundTools import noRound from fontTools.pens.basePen import PenError from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen from fontTools.pens.ttGlyphPen import TTGlyphPen, TTGlyphPointPen, MAX_F2DOT14 @@ -564,6 +565,101 @@ def test_scaled_component_bounds(self): uni0302_uni0300.recalcBounds(glyphSet) self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025)) + def test_unrounded_coords_rounded_bounds(self): + # The following test case is taken from Faustina-Italic.glyphs. + # It reproduces a bug whereby the bounding boxes of composite glyphs that + # contain components with non trivial transformations, and in which the base + # glyphs of the transformed components still has un-rounded, floating-point + # coordinates, may be off-by-one compared to the correct bounds that should + # be computed from the rounded coordinates. + # https://github.com/googlefonts/fontc/issues/1206 + glyphSet = {} + pen = TTGlyphPointPen(glyphSet) + + pen.beginPath() + pen.addPoint((235, 680), "line") + pen.addPoint((292, 729), "line") + pen.addPoint((314.5, 748.5), None) + pen.addPoint((342.0, 780.75), None) + pen.addPoint((342.0, 792.0), "qcurve") + pen.addPoint((342.0, 800.0), None) + pen.addPoint((336.66666666666663, 815.9166666666666), None) + pen.addPoint((318.0, 829.5), None) + pen.addPoint((298.0, 834.0), "qcurve") + pen.addPoint((209, 702), "line") + pen.endPath() + pen.beginPath() + pen.addPoint((90, 680), "line") + pen.addPoint((147, 729), "line") + pen.addPoint((169.5, 748.5), None) + pen.addPoint((197.0, 780.75), None) + pen.addPoint((197.0, 792.0), "qcurve") + pen.addPoint((197.0, 800.0), None) + pen.addPoint((191.66666666666663, 815.9166666666666), None) + pen.addPoint((173.0, 829.5), None) + pen.addPoint((153.0, 834.0), "qcurve") + pen.addPoint((64, 702), "line") + pen.endPath() + # round=noRound forces floating-point coordinates to be kept un-rounded + glyphSet["hungarumlaut.case"] = pen.glyph( + dropImpliedOnCurves=True, round=noRound + ) + + # component has non-trivial transform (shear + reflection) + pen.addComponent("hungarumlaut.case", (-1, 0, 0.28108, 1, 196, 0)) + compositeGlyph = pen.glyph(round=noRound) + glyphSet["dblgravecomb.case"] = compositeGlyph + + compositeGlyph.recalcBounds(glyphSet) + # these are the expected bounds as computed from the rounded coordinates + self.assertGlyphBoundsEqual(compositeGlyph, (74, 680, 329, 834)) + + def test_composite_bounds_with_nested_components(self): + # The following test case is taken from Joan.glyphs at + # https://github.com/PaoloBiagini/Joan + # There's a composite glyph 'bullet.sc' which contains a transformed + # component 'bullet', which in turn is a composite glyph referencing + # another transformed components ('period'). The two transformations + # combine to produce the final transformed coordinates, but the necessary + # rounding should only happen at the end, and not at each intermediate + # level of the nested component tree. + glyphSet = {} + pen = TTGlyphPointPen(glyphSet) + + pen.beginPath() + pen.addPoint((141.0, -8.0), "qcurve") + pen.addPoint((168.0, -8.0), None) + pen.addPoint((205.0, 30.5), None) + pen.addPoint((205.0, 56.0), "qcurve") + pen.addPoint((205.0, 82.25), None) + pen.addPoint((168.0, 118.0), None) + pen.addPoint((141.0, 118.0), "qcurve") + pen.addPoint((114.0, 118.0), None) + pen.addPoint((78.0, 79.5), None) + pen.addPoint((78.0, 54.0), "qcurve") + pen.addPoint((78.0, 27.75), None) + pen.addPoint((114.0, -8.0), None) + pen.endPath() + glyphSet["period"] = pen.glyph() + + pen.addComponent("period", (1.6, 0, 0, 1.6, -41, 140)) + glyphSet["bullet"] = pen.glyph() + + pen.addComponent("bullet", (0.9, 0, 0, 0.9, 9, 49)) + compositeGlyph = glyphSet["bullet.sc"] = pen.glyph() + + # this is the xMin of 'bullet.sc' glyph if coordinates were left unrounded + coords, _, _ = compositeGlyph.getCoordinates(glyphSet) + assert pytest.approx(min(x for x, y in coords)) == 84.42033 + + compositeGlyph.recalcBounds(glyphSet) + + # if rounding happened at intermediate stages (i.e. after extracting transformed + # coordinates of 'period' component from 'bullet', before transforming 'bullet' + # component in 'bullet.sc'), the rounding errors would compound leading to xMin + # incorrectly be equal to 85. Whereas 84.42033 should round down to 84. + self.assertGlyphBoundsEqual(compositeGlyph, (84, 163, 267, 345)) + def test_open_path_starting_with_move(self): # when a contour starts with a 'move' point, it signifies the beginnig # of an open contour. diff --git a/Tests/subset/data/Andika-Regular.subset.ttx b/Tests/subset/data/Andika-Regular.subset.ttx new file mode 100644 index 0000000000..8fd28a7a9d --- /dev/null +++ b/Tests/subset/data/Andika-Regular.subset.ttx @@ -0,0 +1,733 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (c) 2004-2022 SIL International + + + Andika + + + Regular + + + SIL International: Andika Regular: 2022 + + + Andika + + + Version 6.101 + + + Andika + + + Capital Eng + + + Alternate forms of capital Eng + + + Ŋ + + + Lowercase no descender + + + Capital form + + + Lowercase short stem + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/cmap14_font1.no_uvs.ttx b/Tests/subset/data/cmap14_font1.no_uvs.ttx new file mode 100644 index 0000000000..75bd028427 --- /dev/null +++ b/Tests/subset/data/cmap14_font1.no_uvs.ttx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/subset/data/cmap14_font1.no_uvs_non_default.ttx b/Tests/subset/data/cmap14_font1.no_uvs_non_default.ttx new file mode 100644 index 0000000000..48b04de436 --- /dev/null +++ b/Tests/subset/data/cmap14_font1.no_uvs_non_default.ttx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Tests/subset/data/cmap14_font1.ttx b/Tests/subset/data/cmap14_font1.ttx new file mode 100644 index 0000000000..fdb00a4e78 --- /dev/null +++ b/Tests/subset/data/cmap14_font1.ttx @@ -0,0 +1,1101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cmap14_font1 + + + Regular + + + cmap14_font1 + + + cmap14_font1 + + + Version1.0 + + + cmap14_font1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 145 665 rmoveto + -74 -43 -28 -166 -6 -75 -10 -124 21 -220 148 -44 rrcurveto + -11 37 40 7 39 hhcurveto + 6 8 3 4 4 hvcurveto + 69 60 39 31 2 103 1 44 5 43 -4 43 -7 87 -50 217 -88 45 -24 13 -29 1 -28 7 -12 -1 -8 -4 -6 -7 -16 -2 -16 -3 -13 -8 rrcurveto + 122 -50 rmoveto + 97 -69 31 -246 -15 -107 -13 -95 -42 -80 -111 33 -52 16 -30 55 -16 46 -32 98 -1 279 95 68 13 9 18 -2 15 4 1 1 2 1 2 1 14 -2 13 -2 11 -8 rrcurveto + 233 -615 rmoveto + return + + + 175 661 rmoveto + 1 -215 6 -215 1 -215 4 -42 54 3 2 41 -1 216 -6 214 -1 215 -11 35 -42 0 -7 -37 rrcurveto + 325 -661 rmoveto + return + + + 143 536 rmoveto + 59 22 61 39 64 3 78 5 3 -97 -32 -48 -76 -117 -268 -55 -9 -168 -2 -31 11 -30 5 -31 5 -10 5 -5 10 -5 50 -15 58 8 50 1 65 1 66 3 65 1 37 7 0 42 -35 11 rrcurveto + -106 -2 -108 -7 -107 4 -2 18 -8 18 2 17 16 141 259 55 69 117 72 122 -67 142 -156 -40 -52 -14 -48 -26 -51 -19 -40 -14 16 -51 41 8 rrcurveto + 357 -536 rmoveto + return + + + 92 580 rmoveto + 13 6 13 7 14 4 54 16 184 1 9 -81 1 -13 -3 -13 -3 -14 -9 -45 -124 -14 -42 -8 rrcurveto + -2 -2 1 -1 hhcurveto + -2 vlineto + -30 -15 5 -40 35 -4 60 -5 62 -4 47 -43 83 -75 -108 -134 -82 -20 -75 -17 -101 91 -42 -14 -22 -8 -7 -18 10 -21 2 -2 2 -2 1 -2 10 -10 11 -3 10 2 rrcurveto + 2 2 -1 1 hhcurveto + 16 -7 15 -7 15 -7 33 -14 33 -14 35 -7 103 -18 81 94 48 78 51 83 -64 98 -77 36 -4 1 -3 2 -4 2 17 7 16 9 15 12 77 61 -32 107 -79 40 -91 47 -115 -9 -91 -40 rrcurveto + -27 -24 18 -37 36 7 rrcurveto + 408 -580 rmoveto + return + + + 336 627 rmoveto + -73 -94 -78 -92 -70 -97 -32 -45 -39 -39 -2 -56 2 -16 5 -7 14 -7 76 -39 130 16 102 10 -2 -44 -2 -44 -1 -43 4 -42 54 3 2 41 1 45 2 45 2 45 rrcurveto + 6 6 0 1 5 hvcurveto + 41 8 -6 54 -42 -3 rrcurveto + -2 -3 -1 -2 hhcurveto + 4 135 -3 133 -49 127 -2 3 -3 2 -2 2 -6 6 -8 4 -9 -1 rrcurveto + -6 -6 -3 -4 -4 hvcurveto + -2 -1 -1 -1 -1 -1 rrcurveto + -230 -408 rmoveto + 9 14 6 14 9 13 16 24 37 51 17 22 48 64 50 62 50 62 29 -105 1 -110 -4 -109 -87 -9 -131 -13 -50 20 rrcurveto + 394 -219 rmoveto + return + + + 41 642 rmoveto + 1 -2 1 -1 -1 vvcurveto + -7 2 -7 5 -5 vhcurveto + 15 -69 -71 -105 61 -45 71 -50 214 60 48 -116 9 -20 3 -24 -3 -22 -13 -128 -51 -35 -120 -6 -38 -1 -62 -5 -26 34 -29 22 -33 -28 16 -33 39 -51 75 0 59 2 83 5 76 21 49 69 rrcurveto + 25 36 0 48 11 42 19 72 -43 43 -42 45 -62 68 -159 -25 -76 26 -20 43 44 56 -6 66 101 14 102 -5 103 -1 37 7 0 42 -35 11 -109 1 -110 5 -108 -17 rrcurveto + -1 1 0 0 1 vvcurveto + -25 33 -45 -26 18 -38 rrcurveto + 407 -673 rmoveto + return + + + 399 660 rmoveto + -36 2 -37 10 -35 -8 -152 -32 -56 -137 -37 -134 -35 -130 55 -175 141 -42 156 -46 135 253 -64 123 -39 78 -32 -3 -81 14 -26 5 -36 -14 -24 -10 -36 -15 -28 -18 -26 -26 19 101 63 130 114 18 rrcurveto + 32 5 31 -8 32 -1 37 7 0 42 -35 11 rrcurveto + -263 -360 rmoveto + 52 57 149 71 42 -110 33 -84 -77 -193 -113 33 -98 30 -29 103 4 92 9 -7 14 -1 14 9 rrcurveto + 401 -299 rmoveto + return + + + 99 610 rmoveto + 63 14 62 -15 64 -2 rrcurveto + 22 23 1 2 22 hvcurveto + -24 -33 -19 -38 -22 -38 -85 -149 -77 -149 -19 -173 4 -37 43 -4 12 34 19 165 74 145 83 142 34 57 25 61 56 36 21 24 -14 30 -32 -2 rrcurveto + -6 -47 -49 -8 -48 hhcurveto + -71 2 -67 15 -70 -17 -40 -14 16 -51 41 8 rrcurveto + 418 -667 rmoveto + return + + + 289 676 rmoveto + -88 12 -105 -100 -7 -86 -1 -23 -10 -26 9 -22 9 -21 8 -23 13 -20 6 -8 8 -7 9 -5 -42 -15 -31 -26 -21 -57 -31 -83 41 -138 89 -34 25 -9 24 -16 27 1 90 2 -6 -5 70 46 rrcurveto + 60 39 -5 113 -8 58 -2 24 -13 22 -9 22 -8 20 -18 15 -15 16 -7 7 -9 4 -9 3 3 5 3 5 3 6 43 84 -21 87 -3 90 -6 20 -17 8 -14 -3 -10 9 -11 8 -13 1 rrcurveto + -12 -364 rmoveto + 2 -2 2 -1 3 -1 12 -4 13 -1 9 -8 26 -18 13 -38 6 -28 24 -103 -43 -94 -120 16 -104 15 -73 140 80 83 31 33 22 -2 42 7 19 -4 19 3 17 7 rrcurveto + 32 196 rmoveto + 2 -48 -9 -48 -33 -37 -30 -34 -85 64 -8 41 -11 56 73 136 70 -23 8 -3 8 -6 7 -6 2 -31 4 -31 2 -30 rrcurveto + 191 -508 rmoveto + return + + + 379 635 rmoveto + -50 16 -48 25 -52 6 -169 23 -32 -255 81 -95 66 -76 -16 4 97 -2 rrcurveto + 6 9 3 4 4 hvcurveto + 21 21 19 16 16 17 8 -65 4 -65 -6 -62 -4 -33 -9 -54 -40 -14 -66 -23 -78 47 -54 20 -40 13 -19 -50 37 -19 46 -17 45 -17 45 -16 31 -11 34 12 32 2 104 6 0 190 -4 62 rrcurveto + -1 36 -5 36 -5 36 -2 23 -4 24 -3 23 13 51 -17 20 19 51 5 16 -4 13 -9 8 15 11 0 23 -20 16 rrcurveto + -72 -84 rmoveto + 2 -34 4 -35 5 -35 -3 -19 -4 -16 -6 -7 -19 -22 -22 -20 -21 -21 -14 1 -14 0 -15 1 -53 58 -34 59 18 84 5 21 15 17 10 18 21 7 21 16 22 -3 41 -5 38 -19 40 -14 rrcurveto + -1 -2 -2 -1 -1 -2 -14 3 -15 -9 -4 -21 rrcurveto + 193 -551 rmoveto + return + + + f75af910 158c838c 828d8387 5d8a7d7a + 4d5ffb37 3afb2878 fb3e8f66 b68797ad + 92c79ac5 9dc3c287 bf99c18f 9d559f55 + a4569e66 bd9e7eb3 0838f74a 65f7516b + f7570892 8c938c93 1e8da478 977a887a + 8d797d8e 7208acfb 50159847 9b489e49 + 61866381 618ca4d2 a8d2a1d4 08f7a1fc + 54150b + + + c0f8f115 78538277 884f8830 a6318e30 + 8e468891 7e480888 8c878c89 1e867b95 + 78a389c9 91f72b8d c3c1a0a0 a49d9aa4 + c7f22be8 2baea298 a39ba6a1 cfc272f6 + 57be799e 71937497 50aa4068 55790871 + 82897396 7d898989 898a8808 b4fba915 + dd8daf97 d367d665 9f323c5c 47625089 + 428593b6 8e9f89c0 89b584b6 84b708f7 + 28f7bd15 ce79b23d 5852564f 3b7f3f7f + 088a8b8a 8a1b8c84 07898b8a 8c8a1e73 + 7b9168aa 86d696bb 96bda779 9179907a + 8d618f61 85608a86 b98ab995 b990a194 + 9e92a008 928a9188 901ebe9d a79ac37d + 08fb36fb 85158f78 90798f78 088c0688 + 9f889e89 9f08f833 fc17150b + + + f83bf8f9 155ea564 b85791fb 5ba649fc + 1bb1fb10 bafb2b70 a9f70734 08879092 + 89911bda 90d09eb3 cc9ba696 a1a29fa4 + a771ac69 7f7e8080 82807d7d 78745176 + 85698168 83688276 9b6e967d a143f70b + 9df7b1f4 ec089e9d aa8ba393 b175b075 + b075b179 a5b86aa4 08d4fcf9 150b + + + cef8ec15 93948c94 1b6efb1f 9efb1d9d + fb200889 0791578a 998e4408 8807838d + 848c848e 89997f97 778a0888 888b8a88 + 1f808787 88848008 8a8b8a8a 891e887c + 8e829680 df3ff75a cbc2e0e8 f724a2f7 + 0e4ef734 81a6729e 79a14dd6 fb1b7c34 + 81088a85 858b851b 62898855 b58708f7 + 9a6b15f7 052b64fb 6f38266a 624c6e54 + 82088e07 84f7455a f743b4f7 42089007 + c991c88a bf5f08f7 3ffccc15 0b + + + bff8fa15 9f36903a 87338957 88678757 + 08857bfb 4392751e 9669b487 a98a08f3 + ecb091f2 1fb49385 c1618820 85215cfb + 019a94c4 8ac48ec4 8daa8dab 8daabd8d + be90bd8c b0928bb5 6896598a 5a865889 + 8ed988d9 7bd908e8 a0f7088b e7799e77 + ab958dab 8a8e8b8e 8a8e869a 85927b8f + 21a3fb2e 88216d08 7a85857f 801a8a88 + 8b888c88 08f854fc fa150b + + + cef90215 90068e6c 7969876d 876b8c7f + 8a61082d 0783808c 7d93828d fb0190fb + 018cfb01 8f61c18e 8db48af5 85f689f6 + d79ad97b d99caa9d 7fb46789 437b4298 + 44818ab6 8cb68db6 088db59e b48cb2f7 + 0287f584 f702a3aa 9d7fb467 89fb0174 + fb0095fb 028f7ba1 7286817b 67858b5a + b38708f8 45fd0215 0b + + + 500 0 rmoveto + return + + + 0b + + + + + + -91 callsubr + -91 callsubr + endchar + + + -107 callsubr + -106 callsubr + endchar + + + -106 callsubr + -107 callsubr + endchar + + + -106 callsubr + -106 callsubr + endchar + + + -106 callsubr + -105 callsubr + endchar + + + -106 callsubr + -104 callsubr + endchar + + + -106 callsubr + -103 callsubr + endchar + + + -106 callsubr + -102 callsubr + endchar + + + -106 callsubr + -101 callsubr + endchar + + + -106 callsubr + -100 callsubr + endchar + + + -106 callsubr + -99 callsubr + endchar + + + -106 callsubr + -98 callsubr + endchar + + + -107 callsubr + -105 callsubr + endchar + + + -105 callsubr + -107 callsubr + endchar + + + -105 callsubr + -106 callsubr + endchar + + + -105 callsubr + -105 callsubr + endchar + + + -105 callsubr + -104 callsubr + endchar + + + -105 callsubr + -103 callsubr + endchar + + + -105 callsubr + -102 callsubr + endchar + + + -105 callsubr + -101 callsubr + endchar + + + -105 callsubr + -100 callsubr + endchar + + + -105 callsubr + -99 callsubr + endchar + + + -105 callsubr + -98 callsubr + endchar + + + -107 callsubr + -104 callsubr + endchar + + + -104 callsubr + -107 callsubr + endchar + + + -104 callsubr + -106 callsubr + endchar + + + -104 callsubr + -105 callsubr + endchar + + + -104 callsubr + -104 callsubr + endchar + + + -104 callsubr + -103 callsubr + endchar + + + -104 callsubr + -102 callsubr + endchar + + + -104 callsubr + -101 callsubr + endchar + + + -104 callsubr + -100 callsubr + endchar + + + -104 callsubr + -99 callsubr + endchar + + + -104 callsubr + -98 callsubr + endchar + + + -107 callsubr + -103 callsubr + endchar + + + -103 callsubr + -107 callsubr + endchar + + + -103 callsubr + -106 callsubr + endchar + + + -103 callsubr + -105 callsubr + endchar + + + -103 callsubr + -104 callsubr + endchar + + + -103 callsubr + -103 callsubr + endchar + + + -103 callsubr + -102 callsubr + endchar + + + -103 callsubr + -101 callsubr + endchar + + + -103 callsubr + -100 callsubr + endchar + + + -103 callsubr + -99 callsubr + endchar + + + -103 callsubr + -98 callsubr + endchar + + + -107 callsubr + -102 callsubr + endchar + + + -102 callsubr + -107 callsubr + endchar + + + -102 callsubr + -106 callsubr + endchar + + + -102 callsubr + -105 callsubr + endchar + + + -102 callsubr + -104 callsubr + endchar + + + -102 callsubr + -103 callsubr + endchar + + + -102 callsubr + -102 callsubr + endchar + + + -102 callsubr + -101 callsubr + endchar + + + -102 callsubr + -100 callsubr + endchar + + + -102 callsubr + -99 callsubr + endchar + + + -102 callsubr + -98 callsubr + endchar + + + -107 callsubr + -101 callsubr + endchar + + + -101 callsubr + -107 callsubr + endchar + + + -101 callsubr + -106 callsubr + endchar + + + -101 callsubr + -105 callsubr + endchar + + + -101 callsubr + -104 callsubr + endchar + + + -101 callsubr + -103 callsubr + endchar + + + -101 callsubr + -102 callsubr + endchar + + + -101 callsubr + -101 callsubr + endchar + + + -101 callsubr + -100 callsubr + endchar + + + -101 callsubr + -99 callsubr + endchar + + + -101 callsubr + -98 callsubr + endchar + + + -107 callsubr + -100 callsubr + endchar + + + -100 callsubr + -107 callsubr + endchar + + + -100 callsubr + -106 callsubr + endchar + + + -100 callsubr + -105 callsubr + endchar + + + -100 callsubr + -104 callsubr + endchar + + + -100 callsubr + -103 callsubr + endchar + + + -100 callsubr + -102 callsubr + endchar + + + -100 callsubr + -101 callsubr + endchar + + + -100 callsubr + -100 callsubr + endchar + + + -100 callsubr + -99 callsubr + endchar + + + -100 callsubr + -98 callsubr + endchar + + + -107 callsubr + -99 callsubr + endchar + + + -99 callsubr + -107 callsubr + endchar + + + -99 callsubr + -106 callsubr + endchar + + + -99 callsubr + -105 callsubr + endchar + + + -99 callsubr + -104 callsubr + endchar + + + -99 callsubr + -103 callsubr + endchar + + + -99 callsubr + -102 callsubr + endchar + + + -99 callsubr + -101 callsubr + endchar + + + -99 callsubr + -100 callsubr + endchar + + + -99 callsubr + -99 callsubr + endchar + + + -99 callsubr + -98 callsubr + endchar + + + -107 callsubr + -98 callsubr + endchar + + + -98 callsubr + -107 callsubr + endchar + + + -98 callsubr + -106 callsubr + endchar + + + -98 callsubr + -105 callsubr + endchar + + + -98 callsubr + -104 callsubr + endchar + + + -98 callsubr + -103 callsubr + endchar + + + -98 callsubr + -102 callsubr + endchar + + + -98 callsubr + -101 callsubr + endchar + + + -98 callsubr + -100 callsubr + endchar + + + -98 callsubr + -99 callsubr + endchar + + + -98 callsubr + -98 callsubr + endchar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/subset/data/cmap14_font1.uvs.ttx b/Tests/subset/data/cmap14_font1.uvs.ttx new file mode 100644 index 0000000000..f241c4763e --- /dev/null +++ b/Tests/subset/data/cmap14_font1.uvs.ttx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Tests/subset/data/cmap14_font1.uvs_non_default.ttx b/Tests/subset/data/cmap14_font1.uvs_non_default.ttx new file mode 100644 index 0000000000..07c4ba0cf6 --- /dev/null +++ b/Tests/subset/data/cmap14_font1.uvs_non_default.ttx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + 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/subset/subset_test.py b/Tests/subset/subset_test.py index abb82687e3..416b26da50 100644 --- a/Tests/subset/subset_test.py +++ b/Tests/subset/subset_test.py @@ -938,6 +938,34 @@ def test_cmap_prune_format12(self): subsetfont = TTFont(subsetpath) self.expect_ttx(subsetfont, self.getpath("CmapSubsetTest.subset.ttx"), ["cmap"]) + def test_cmap_format14(self): + fontpath = self.compile_font(self.getpath("cmap14_font1.ttx"), ".otf") + subsetpath = self.temp_path(".otf") + + subset.main([fontpath, "--unicodes=4e05", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("cmap14_font1.no_uvs.ttx"), ["cmap"]) + + subset.main( + [fontpath, "--unicodes=4e05,e0100", "--output-file=%s" % subsetpath] + ) + subsetfont = TTFont(subsetpath) + self.expect_ttx(subsetfont, self.getpath("cmap14_font1.uvs.ttx"), ["cmap"]) + + subset.main([fontpath, "--unicodes=4e10", "--output-file=%s" % subsetpath]) + subsetfont = TTFont(subsetpath) + self.expect_ttx( + subsetfont, self.getpath("cmap14_font1.no_uvs_non_default.ttx"), ["cmap"] + ) + + subset.main( + [fontpath, "--unicodes=4e10,e0100", "--output-file=%s" % subsetpath] + ) + subsetfont = TTFont(subsetpath) + self.expect_ttx( + subsetfont, self.getpath("cmap14_font1.uvs_non_default.ttx"), ["cmap"] + ) + @pytest.mark.parametrize("text, n", [("!", 1), ("#", 2)]) def test_GPOS_PairPos_Format2_useClass0(self, text, n): # Check two things related to class 0 ('every other glyph'): @@ -1752,7 +1780,7 @@ def test_subset_COLRv1_drop_all_v0_glyphs(colrv1_path): assert colr.table.BaseGlyphRecordCount == 0 assert colr.table.BaseGlyphRecordArray is None assert colr.table.LayerRecordArray is None - assert colr.table.LayerRecordCount is 0 + assert colr.table.LayerRecordCount == 0 def test_subset_COLRv1_no_ClipList(colrv1_path): @@ -2071,5 +2099,28 @@ def test_prune_unused_user_name_IDs_with_keep_all(ttf_path): assert nameIDs == keepNameIDs +def test_cvXX_feature_params_nameIDs_are_retained(): + # https://github.com/fonttools/fonttools/issues/3616 + font = TTFont() + ttx = pathlib.Path(__file__).parent / "data" / "Andika-Regular.subset.ttx" + font.importXML(ttx) + + keepNameIDs = {n.nameID for n in font["name"].names} + + options = subset.Options() + options.glyph_names = True + # that's where the FeatureParamsCharacteVariants are stored + options.layout_features.append("cv43") + + subsetter = subset.Subsetter(options) + subsetter.populate(glyphs=font.getGlyphOrder()) + subsetter.subset(font) + + # we expect that all nameIDs are retained, including all the nameIDs + # used by the FeatureParamsCharacterVariants + nameIDs = {n.nameID for n in font["name"].names} + assert nameIDs == keepNameIDs + + if __name__ == "__main__": sys.exit(unittest.main()) diff --git a/Tests/ttLib/data/IBMPlexSans-Bold.subset.otf b/Tests/ttLib/data/IBMPlexSans-Bold.subset.otf new file mode 100644 index 0000000000..23a0301fc1 Binary files /dev/null and b/Tests/ttLib/data/IBMPlexSans-Bold.subset.otf differ 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/removeOverlaps_test.py b/Tests/ttLib/removeOverlaps_test.py index 1320c9be5c..33493f68fd 100644 --- a/Tests/ttLib/removeOverlaps_test.py +++ b/Tests/ttLib/removeOverlaps_test.py @@ -1,9 +1,13 @@ import logging import pytest +from pathlib import Path pathops = pytest.importorskip("pathops") -from fontTools.ttLib.removeOverlaps import _simplify, _round_path +from fontTools.ttLib import TTFont +from fontTools.ttLib.removeOverlaps import removeOverlaps, _simplify, _round_path + +DATA_DIR = Path(__file__).parent / "data" def test_pathops_simplify_bug_workaround(caplog): @@ -49,3 +53,21 @@ def test_pathops_simplify_bug_workaround(caplog): expected.close() assert expected == _round_path(result, round=lambda v: round(v, 3)) + + +def test_CFF_CharString_width_nominalWidthX(): + font_path = DATA_DIR / "IBMPlexSans-Bold.subset.otf" + font = TTFont(str(font_path)) + + assert font["hmtx"]["OE"][0] == 998 + + # calcBounds() has the side effect of setting the width attribute + font["CFF "].cff[0].CharStrings["OE"].calcBounds({}) + assert font["CFF "].cff[0].CharStrings["OE"].width == font["hmtx"]["OE"][0] + + removeOverlaps(font) + + assert font["hmtx"]["OE"][0] == 998 + + font["CFF "].cff[0].CharStrings["OE"].calcBounds({}) + assert font["CFF "].cff[0].CharStrings["OE"].width == font["hmtx"]["OE"][0] diff --git a/Tests/ttLib/reorderGlyphs_test.py b/Tests/ttLib/reorderGlyphs_test.py index cfda2d285c..02ac2f49d2 100644 --- a/Tests/ttLib/reorderGlyphs_test.py +++ b/Tests/ttLib/reorderGlyphs_test.py @@ -59,6 +59,17 @@ def test_ttfont_reorder_glyphs(): assert list(reversed(old_coverage2)) == new_coverage2 +def test_reorder_glyphs_cff(): + font_path = DATA_DIR / "TestVGID-Regular.otf" + font = TTFont(str(font_path)) + ga = font.getGlyphOrder() + ga = list(reversed(ga)) + reorderGlyphs(font, ga) + + assert list(font["CFF "].cff.topDictIndex[0].CharStrings.charStrings.keys()) == ga + assert font["CFF "].cff.topDictIndex[0].charset == ga + + def test_reorder_glyphs_bad_length(caplog): font_path = DATA_DIR / "Test-Regular.ttf" font = TTFont(str(font_path)) diff --git a/Tests/ttLib/tables/D__e_b_g_test.py b/Tests/ttLib/tables/D__e_b_g_test.py new file mode 100644 index 0000000000..b4e0135175 --- /dev/null +++ b/Tests/ttLib/tables/D__e_b_g_test.py @@ -0,0 +1,52 @@ +from fontTools.misc.testTools import parseXmlInto, getXML +from fontTools.ttLib.tables.D__e_b_g import table_D__e_b_g + + +DEBG_DATA = { + "com.github.fonttools.feaLib": { + "GPOS": {"0": [":6:5", "kern_Default", ["DFLT", "dflt", "kern"]]} + } +} + + +DEBG_XML = """\ + + :6:5", + "kern_Default", + [ + "DFLT", + "dflt", + "kern" + ] + ] + } + } + }]]> + +""" + + +def test_compile_decompile_and_roundtrip_ttx(): + ttFont = None + table = table_D__e_b_g() + assert table.tableTag == "Debg" + assert table.data == {} + table.data = DEBG_DATA + + data = table.compile(ttFont) + + table2 = table_D__e_b_g() + table2.decompile(data, ttFont) + + assert table.data == table2.data + + assert getXML(table2.toXML) == DEBG_XML.splitlines() + + table3 = table_D__e_b_g() + parseXmlInto(ttFont, table3, DEBG_XML) + + assert table3.data == DEBG_DATA 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/TupleVariation_test.py b/Tests/ttLib/tables/TupleVariation_test.py index bfb0e453bb..ffd4723783 100644 --- a/Tests/ttLib/tables/TupleVariation_test.py +++ b/Tests/ttLib/tables/TupleVariation_test.py @@ -594,8 +594,8 @@ def test_compileDeltas_constants(self): self.assertEqual("02 01 02 04", hexencode(var.compileDeltas())) def test_compileDeltaValues(self): - compileDeltaValues = lambda values: hexencode( - TupleVariation.compileDeltaValues_(values) + compileDeltaValues = lambda values, optimizeSize=True: hexencode( + TupleVariation.compileDeltaValues_(values, optimizeSize=optimizeSize) ) # zeroes self.assertEqual("80", compileDeltaValues([0])) @@ -635,18 +635,34 @@ def test_compileDeltaValues(self): ) # bytes, zeroes self.assertEqual("01 01 00", compileDeltaValues([1, 0])) + self.assertEqual("01 01 00", compileDeltaValues([1, 0], optimizeSize=False)) self.assertEqual("00 01 81", compileDeltaValues([1, 0, 0])) + self.assertEqual( + "02 01 00 00", compileDeltaValues([1, 0, 0], optimizeSize=False) + ) # words, bytes, words: a single byte is more compact when encoded as part of the words run self.assertEqual( "42 66 66 00 02 77 77", compileDeltaValues([0x6666, 2, 0x7777]) ) + self.assertEqual( + "42 66 66 00 02 77 77", + compileDeltaValues([0x6666, 2, 0x7777], optimizeSize=False), + ) self.assertEqual( "40 66 66 01 02 02 40 77 77", compileDeltaValues([0x6666, 2, 2, 0x7777]) ) + self.assertEqual( + "43 66 66 00 02 00 02 77 77", + compileDeltaValues([0x6666, 2, 2, 0x7777], optimizeSize=False), + ) # words, zeroes, words self.assertEqual( "40 66 66 80 40 77 77", compileDeltaValues([0x6666, 0, 0x7777]) ) + self.assertEqual( + "42 66 66 00 00 77 77", + compileDeltaValues([0x6666, 0, 0x7777], optimizeSize=False), + ) self.assertEqual( "40 66 66 81 40 77 77", compileDeltaValues([0x6666, 0, 0, 0x7777]) ) 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 9a3fd2eaf3..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) - # 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) + + 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) + + 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) class GlyphComponentTest: @@ -707,6 +796,37 @@ def test_toXML_reference_points(self): '' ] + def test_compile_for_speed(self): + glyph = Glyph() + glyph.numberOfContours = 1 + glyph.coordinates = GlyphCoordinates( + [(0, 0), (1, 0), (1, 0), (1, 1), (1, 1), (0, 1), (0, 1)] + ) + glyph.flags = array.array("B", [flagOnCurve] + [flagCubic] * 6) + glyph.endPtsOfContours = [6] + glyph.program = ttProgram.Program() + + glyph.expand(None) + sizeBytes = glyph.compile(None, optimizeSize=True) + glyph.expand(None) + speedBytes = glyph.compile(None, optimizeSize=False) + + assert len(sizeBytes) < len(speedBytes) + + for data in sizeBytes, speedBytes: + glyph = Glyph(data) + + pen = RecordingPen() + glyph.draw(pen, None) + + assert pen.value == [ + ("moveTo", ((0, 0),)), + ("curveTo", ((1, 0), (1, 0), (1.0, 0.5))), + ("curveTo", ((1, 1), (1, 1), (0.5, 1.0))), + ("curveTo", ((0, 1), (0, 1), (0, 0))), + ("closePath", ()), + ] + def test_fromXML_reference_points(self): comp = GlyphComponent() for name, attrs, content in parseXML( diff --git a/Tests/ttLib/tables/_n_a_m_e_test.py b/Tests/ttLib/tables/_n_a_m_e_test.py index 6b3a0a70dd..d14b7113c9 100644 --- a/Tests/ttLib/tables/_n_a_m_e_test.py +++ b/Tests/ttLib/tables/_n_a_m_e_test.py @@ -1,7 +1,7 @@ from fontTools.misc import sstruct from fontTools.misc.loggingTools import CapturingLogHandler from fontTools.misc.testTools import FakeFont -from fontTools.misc.textTools import bytesjoin, tostr +from fontTools.misc.textTools import bytesjoin, tostr, Tag from fontTools.misc.xmlWriter import XMLWriter from io import BytesIO import struct @@ -27,6 +27,11 @@ def names(nameTable): class NameTableTest(unittest.TestCase): + def test_init(self): + table = table__n_a_m_e() + self.assertEqual(table.names, []) + self.assertTrue(type(table.tableTag) is Tag) + def test_getDebugName(self): table = table__n_a_m_e() table.names = [ diff --git a/Tests/ttLib/tables/otTables_test.py b/Tests/ttLib/tables/otTables_test.py index db33e4c659..01bc97db81 100644 --- a/Tests/ttLib/tables/otTables_test.py +++ b/Tests/ttLib/tables/otTables_test.py @@ -4,6 +4,7 @@ from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter import fontTools.ttLib.tables.otTables as otTables from io import StringIO +from textwrap import dedent import unittest @@ -735,6 +736,93 @@ def test_traverseEmptyPaintColrLayersNeedsNoLayerList(self): assert len(visited) == 1 +def test_parse_Device_DeltaValue_from_XML_and_compile(): + # https://github.com/fonttools/fonttools/pull/3757 + font = FakeFont([".notdef", "five"]) + + gpos_xml = dedent( + """\ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ + ) + + gpos = parseXmlInto(font, otTables.GPOS(), gpos_xml) + + anchor = gpos.LookupList.Lookup[0].SubTable[0].EntryExitRecord[0].EntryAnchor + assert anchor.XDeviceTable.DeltaValue == [1, 2] + assert anchor.YDeviceTable.DeltaValue == [3] + + writer = OTTableWriter() + gpos.compile(writer, font) + data = writer.getAllData() + + reader = OTTableReader(data, tableTag="GPOS") + gpos2 = otTables.GPOS() + gpos2.decompile(reader, font) + + assert dedent("\n".join(getXML(gpos2.toXML, font)[1:-1])) == gpos_xml + + if __name__ == "__main__": import sys 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/ttx/ttx_test.py b/Tests/ttx/ttx_test.py index b3b3a13a82..c588a1e176 100644 --- a/Tests/ttx/ttx_test.py +++ b/Tests/ttx/ttx_test.py @@ -1058,6 +1058,19 @@ def test_roundtrip_DSIG_split_at_XML_parse_buffer_size(tmp_path): ) +def test_main_ttx_compile_optimize_font_speed(tmp_path): + inpath = Path("Tests") / "ttx" / "data" / "TestTTF.ttx" + + size_optimized = tmp_path / "TestTTF-size.ttf" + ttx.main(["-o", str(size_optimized), str(inpath)]) + + speed_optimized = tmp_path / "TestTTF-speed.ttf" + ttx.main(["--optimize-font-speed", "-o", str(speed_optimized), str(inpath)]) + + # the speed-optimized font should end up larger than the size-optimized one + assert size_optimized.stat().st_size < speed_optimized.stat().st_size + + # --------------------------- # support functions for tests # --------------------------- diff --git a/Tests/ufoLib/GLIF1_test.py b/Tests/ufoLib/GLIF1_test.py index c4991ca3e2..64af3f6a27 100644 --- a/Tests/ufoLib/GLIF1_test.py +++ b/Tests/ufoLib/GLIF1_test.py @@ -300,6 +300,22 @@ def testUnicodes_illegal(self): self.assertRaises(GlifLibError, self.pyToGLIF, py) self.assertRaises(GlifLibError, self.glifToPy, glif) + def testUnicodes_hex_present(self): + """Test that a present element must have a + 'hex' attribute; by testing that an invalid + element raises an appropriate error. + """ + + # illegal + glif = """ + + + + + + """ + self.assertRaises(GlifLibError, self.glifToPy, glif) + def testNote(self): glif = """ diff --git a/Tests/ufoLib/GLIF2_test.py b/Tests/ufoLib/GLIF2_test.py index d8c96d6537..a193ab5c58 100644 --- a/Tests/ufoLib/GLIF2_test.py +++ b/Tests/ufoLib/GLIF2_test.py @@ -300,6 +300,22 @@ def testUnicodes_illegal(self): self.assertRaises(GlifLibError, self.pyToGLIF, py) self.assertRaises(GlifLibError, self.glifToPy, glif) + def testUnicodes_hex_present(self): + """Test that a present element must have a + 'hex' attribute; by testing that an invalid + element raises an appropriate error. + """ + + # illegal + glif = """ + + + + + + """ + self.assertRaises(GlifLibError, self.glifToPy, glif) + def testNote(self): glif = """ diff --git a/Tests/unicodedata_test.py b/Tests/unicodedata_test.py index 77301f4dbc..f06a643e36 100644 --- a/Tests/unicodedata_test.py +++ b/Tests/unicodedata_test.py @@ -155,6 +155,33 @@ def test_script(): def test_script_extension(): + assert unicodedata.script_extension("\u00B7") == { + "Avst", + "Cari", + "Copt", + "Dupl", + "Elba", + "Geor", + "Glag", + "Gong", + "Goth", + "Grek", + "Hani", + "Latn", + "Lydi", + "Mahj", + "Perm", + "Shaw", + } + assert unicodedata.script_extension("\u02BC") == { + "Beng", + "Cyrl", + "Deva", + "Latn", + "Lisu", + "Thai", + "Toto", + } assert unicodedata.script_extension("a") == {"Latn"} assert unicodedata.script_extension(chr(0)) == {"Zyyy"} assert unicodedata.script_extension(chr(0x0378)) == {"Zzzz"} @@ -182,6 +209,7 @@ def test_script_extension(): "Taml", "Telu", "Tirh", + "Onao", } diff --git a/Tests/varLib/avar_test.py b/Tests/varLib/avar_test.py new file mode 100644 index 0000000000..92f7b3a7d5 --- /dev/null +++ b/Tests/varLib/avar_test.py @@ -0,0 +1,94 @@ +from fontTools.ttLib import TTFont +from fontTools.varLib.models import VariationModel +from fontTools.varLib.avar import _pruneLocations, mappings_from_avar +import os +import unittest +import pytest + +TESTS = [ + ( + [ + {"wght": 1}, + {"wght": 0.5}, + ], + [ + {"wght": 0.5}, + ], + [ + {"wght": 0.5}, + ], + ), + ( + [ + {"wght": 1, "wdth": 1}, + {"wght": 0.5, "wdth": 1}, + ], + [ + {"wght": 1, "wdth": 1}, + ], + [ + {"wght": 1, "wdth": 1}, + {"wght": 0.5, "wdth": 1}, + ], + ), + ( + [ + {"wght": 1}, + {"wdth": 1}, + {"wght": 0.5, "wdth": 0.5}, + ], + [ + {"wght": 0.5, "wdth": 0.5}, + ], + [ + {"wght": 0.5, "wdth": 0.5}, + ], + ), +] + + +@pytest.mark.parametrize("locations, poles, expected", TESTS) +def test_pruneLocations(locations, poles, expected): + axisTags = set() + for location in locations: + axisTags.update(location.keys()) + axisTags = sorted(axisTags) + + locations = [{}] + locations + + pruned = _pruneLocations(locations, poles, axisTags) + + assert pruned == expected, (pruned, expected) + + +@pytest.mark.parametrize("locations, poles, expected", TESTS) +def test_roundtrip(locations, poles, expected): + axisTags = set() + for location in locations: + axisTags.update(location.keys()) + axisTags = sorted(axisTags) + + locations = [{}] + locations + expected = [{}] + expected + + model1 = VariationModel(locations, axisTags) + model2 = VariationModel(expected, axisTags) + + for location in poles: + i = model1.locations.index(location) + support1 = model1.supports[i] + + i = model2.locations.index(location) + support2 = model2.supports[i] + + assert support1 == support2, (support1, support2) + + +def test_mappings_from_avar(): + CWD = os.path.abspath(os.path.dirname(__file__)) + DATADIR = os.path.join(CWD, "..", "ttLib", "tables", "data") + varfont_path = os.path.join(DATADIR, "Amstelvar-avar2.subset.ttf") + font = TTFont(varfont_path) + mappings = mappings_from_avar(font) + + assert len(mappings) == 2, mappings 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/MutatorSans_All_Variable.ttx b/Tests/varLib/data/MutatorSans_All_Variable.ttx new file mode 100644 index 0000000000..d765b317bf --- /dev/null +++ b/Tests/varLib/data/MutatorSans_All_Variable.ttx @@ -0,0 +1,6671 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + License same as MutatorMath. BSD 3-clause. [test-token: C] + + + Regular + + + 1.002;LTTR;MutatorMathTest-LightCondensed + + + MutatorSans LightCondensed + + + Version 1.002 + + + MutatorMathTest-LightCondensed + + + License same as MutatorMath. BSD 3-clause. [test-token: C] + + + MutatorSans + + + LightCondensed + + + Width + + + Weight + + + MutatorMathTest-LightCondensed + + + BoldCondensed + + + MutatorMathTest-BoldCondensed + + + LightWide + + + MutatorMathTest-LightWide + + + BoldWide + + + MutatorMathTest-BoldWide + + + Medium_Narrow_I + + + MutatorMathTest-Medium_Narrow_I + + + Two + + + MutatorMathTest-Two + + + One + + + MutatorMathTest-One + + + UserLocation_700 + + + MutatorSans-UserLocation_700 + + + UserLocation_100 + + + Medium_Wide_I + + + MutatorMathTest-Medium_Narrow_I + + + Anisotropic_one + + + Support_Layer_Demo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wdth + 0x0 + 0.0 + 0.0 + 1000.0 + 256 + + + + + wght + 0x0 + 0.0 + 0.0 + 1000.0 + 257 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/data/test_results/Build.ttx b/Tests/varLib/data/test_results/Build.ttx index 144cca5e75..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 @@ - + - - - - - + + + + + + - - - - - - - - @@ -161,42 +154,42 @@ - + - + - + - + - + - + diff --git a/Tests/varLib/data/test_results/BuildAvar2.ttx b/Tests/varLib/data/test_results/BuildAvar2.ttx index 27a41bfbbf..493d9bdbba 100644 --- a/Tests/varLib/data/test_results/BuildAvar2.ttx +++ b/Tests/varLib/data/test_results/BuildAvar2.ttx @@ -23,7 +23,7 @@ - + diff --git a/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx b/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx index ce5b55d291..b624a657ab 100644 --- a/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx +++ b/Tests/varLib/data/test_results/BuildGvarCompositeExplicitDelta.ttx @@ -176,17 +176,6 @@ - - - - - - - - - - - diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx index 3a1bcfd375..c82323e425 100644 --- a/Tests/varLib/data/test_results/BuildMain.ttx +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -1,5 +1,5 @@ - + @@ -443,63 +443,6 @@ کنتراست - - Weight - - - Contrast - - - ExtraLight - - - TestFamily-ExtraLight - - - Light - - - TestFamily-Light - - - Regular - - - TestFamily-Regular - - - Semibold - - - TestFamily-Semibold - - - Bold - - - TestFamily-Bold - - - Black - - - TestFamily-Black - - - Black Medium Contrast - - - TestFamily-BlackMediumContrast - - - Black High Contrast - - - TestFamily-BlackHighContrast - - - Kontrast - Kontrast @@ -546,39 +489,36 @@ TestFamily-Light - Regular - - TestFamily-Regular - + Semibold - + TestFamily-Semibold - + Bold - + TestFamily-Bold - + Black - + TestFamily-Black - + Black Medium Contrast - + TestFamily-BlackMediumContrast - + Black High Contrast - + TestFamily-BlackHighContrast @@ -657,26 +597,19 @@ - + - - - - - + + + + + + - - - - - - - - @@ -807,42 +740,42 @@ - + - + - + - + - + - + diff --git a/Tests/varLib/data/test_results/DropOnCurves.ttx b/Tests/varLib/data/test_results/DropOnCurves.ttx index 4bfd36ad0a..58a830f3c2 100644 --- a/Tests/varLib/data/test_results/DropOnCurves.ttx +++ b/Tests/varLib/data/test_results/DropOnCurves.ttx @@ -228,9 +228,6 @@ - - Weight - Test Family diff --git a/Tests/varLib/data/test_results/SingleMaster.ttx b/Tests/varLib/data/test_results/SingleMaster.ttx index 02cfe32bd2..47f957743d 100644 --- a/Tests/varLib/data/test_results/SingleMaster.ttx +++ b/Tests/varLib/data/test_results/SingleMaster.ttx @@ -68,9 +68,6 @@ - - Weight - Test Family diff --git a/Tests/varLib/data/test_results/SparseMasters.ttx b/Tests/varLib/data/test_results/SparseMasters.ttx index 2871e24fbd..a35260029e 100644 --- a/Tests/varLib/data/test_results/SparseMasters.ttx +++ b/Tests/varLib/data/test_results/SparseMasters.ttx @@ -236,9 +236,6 @@ - - Weight - Layer Font diff --git a/Tests/varLib/data/variable_ttx_interpolatable_cff2/interpolatable-test.ttx b/Tests/varLib/data/variable_ttx_interpolatable_cff2/interpolatable-test.ttx new file mode 100644 index 0000000000..f3e7b4291a --- /dev/null +++ b/Tests/varLib/data/variable_ttx_interpolatable_cff2/interpolatable-test.ttx @@ -0,0 +1,1545 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + © 2014 - 2023 Adobe (http://www.adobe.com/), with Reserved Font Name ‘Source’. + + + Source Serif 4 Variable + + + Italic + + + 4.005;ADBO;SourceSerif4Variable-Italic;ADOBE + + + Source Serif 4 Variable Italic + + + Version 4.005;hotconv 1.1.0;makeotfexe 2.6.0 + + + SourceSerif4Variable-Italic + + + Italic + + + Italic + + + ExtraLight + + + Light + + + Regular + + + Medium + + + Semibold + + + Bold + + + ExtraBold + + + Black + + + Caption + + + SmallText + + + Text + + + Subhead + + + Display + + + Italic + + + Weight + + + Optical Size + + + Caption ExtraLight Italic + + + SourceSerif4Variable-CaptionExtraLightItalic + + + Caption Light Italic + + + SourceSerif4Variable-CaptionLightItalic + + + Caption Italic + + + SourceSerif4Variable-CaptionRegularItalic + + + Caption Semibold Italic + + + SourceSerif4Variable-CaptionSemiboldItalic + + + Caption Bold Italic + + + SourceSerif4Variable-CaptionBoldItalic + + + Caption Black Italic + + + SourceSerif4Variable-CaptionBlackItalic + + + SmText ExtraLight Italic + + + SourceSerif4Variable-SmTextExtraLightItalic + + + SmText Light Italic + + + SourceSerif4Variable-SmTextLightItalic + + + SmText Italic + + + SourceSerif4Variable-SmTextRegularItalic + + + SmText Semibold Italic + + + SourceSerif4Variable-SmTextSemiboldItalic + + + SmText Bold Italic + + + SourceSerif4Variable-SmTextBoldItalic + + + SmText Black Italic + + + SourceSerif4Variable-SmTextBlackItalic + + + ExtraLight Italic + + + SourceSerif4Variable-ExtraLightItalic + + + Light Italic + + + SourceSerif4Variable-LightItalic + + + SourceSerif4Variable-Italic + + + Semibold Italic + + + SourceSerif4Variable-SemiboldItalic + + + Bold Italic + + + SourceSerif4Variable-BoldItalic + + + Black Italic + + + SourceSerif4Variable-BlackItalic + + + Subhead ExtraLight Italic + + + SourceSerif4Variable-SubheadExtraLightItalic + + + Subhead Light Italic + + + SourceSerif4Variable-SubheadLightItalic + + + Subhead Italic + + + SourceSerif4Variable-SubheadRegularItalic + + + Subhead Semibold Italic + + + SourceSerif4Variable-SubheadSemiboldItalic + + + Subhead Bold Italic + + + SourceSerif4Variable-SubheadBoldItalic + + + Subhead Black Italic + + + SourceSerif4Variable-SubheadBlackItalic + + + Display ExtraLight Italic + + + SourceSerif4Variable-DisplayExtraLightItalic + + + Display Light Italic + + + SourceSerif4Variable-DisplayLightItalic + + + Display Italic + + + SourceSerif4Variable-DisplayRegularItalic + + + Display Semibold Italic + + + SourceSerif4Variable-DisplaySemiboldItalic + + + Display Bold Italic + + + SourceSerif4Variable-DisplayBoldItalic + + + Display Black Italic + + + SourceSerif4Variable-DisplayBlackItalic + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 50 569 51 -28 20 0 1 0 -1 0 -1 63 -57 1 0 -1 0 -1 0 -29 19 -1 -1 1 1 1 1 3 blend + hstem + 89 60 360 60 18 -6 17 13 -20 -19 6 7 -35 20 0 1 0 -1 0 -1 70 -40 0 -2 0 2 0 2 -35 20 0 1 0 -1 0 -1 4 blend + vstem + 89 18 -6 17 13 -20 -19 6 7 1 blend + hmoveto + 61 -36 39 -1 0 1 0 1 0 1 blend + hlineto + 419 670 -61 36 -39 1 0 -1 0 -1 0 6 -18 0 0 0 0 0 0 36 -39 1 0 -1 0 -1 0 3 blend + 0 -419 -670 -36 39 -1 0 1 0 1 0 -6 18 0 0 0 0 0 0 2 blend + rlineto + 480 hmoveto + -419 670 -61 -36 39 -1 0 1 0 1 0 6 -18 0 0 0 0 0 0 36 -39 1 0 -1 0 -1 0 3 blend + 0 419 -670 36 -39 1 0 -1 0 -1 0 -6 18 0 0 0 0 0 0 2 blend + rlineto + 61 -36 39 -1 0 1 0 1 0 1 blend + hlineto + -420 50 -35 20 0 1 0 -1 0 -1 -28 20 0 1 0 -1 0 -1 2 blend + rmoveto + 569 360 -569 -360 63 -57 1 0 -1 0 -1 0 70 -40 0 -2 0 2 0 2 -63 57 -1 0 1 0 1 0 -70 40 0 2 0 -2 0 -2 4 blend + vlineto + -60 -50 35 -20 0 -1 0 1 0 1 28 -20 0 -1 0 1 0 1 2 blend + rmoveto + 480 670 6 -18 0 0 0 0 0 0 1 blend + -480 -670 -6 18 0 0 0 0 0 0 1 blend + hlineto + + + -134 97 660.6587 46.3413 13 -18 2 -11 -2 12 -4 11 -43 40 -1 -11 0 1 3 -1 59.0837 -57.2318 -14.6587 47.5829 10.9163 -28.1159 2.2318 -0.9809 -23.0837 17.2318 13.6587 -25.5829 -8.9163 15.1159 -1.2318 -9.0191 4 blend + hstem + -115 502 26 -17 20 1 19 2 -4 -4 -18 57 18 -19 -41 -15 9 1 2 blend + vstem + -115 -83 26 -17 20 1 19 2 -4 -4 -5 0 3 -16 -5 9 -11 18 2 blend + rmoveto + -35 3 43 -16 46 12 -13 7 3 -1 3 -4 4 4 12 2 0 -9 -4 -2 -14 -13 -1 -14 -12 5 7 13 15 6 -5 -8 2 4 0 11 -11 1 8 16 -6 -20 -18 -1 3 5 blend + hhcurveto + 64 77 54 100 43 12 19 50 -15 -37 -28 -31 -4 -4 -6 -21 -44 -5 9 21 13 13 -8 39 -38 -43 -10 -25 19 40 -22 -37 -68 -39 -39 30 32 -13 10 -11 -8 6 14 4 2 5 blend + hvcurveto + 18 39 11 49 14 75 93 487 -5 24 15 45 -6 -3 -16 16 18 21 27 16 -28 -22 -24 17 -1 12 -6 5 15 14 15 9 -9 45 33 11 31 37 -29 64 8 20 12 27 -3 -25 -19 -37 44 91 58 126 -1 -121 -73 -173 -23 -20 -23 0 11 22 16 -2 -113 -127 -122 -36 82 143 125 30 8 blend + rcurveline + -90 58 -91 -6 20 4 -17 -9 -3 1 blend + hlineto + -15 -104 -20 -104 -18 -102 -30 -173 -9 -60 -14 -67 -15 -71 -24 -54 -42 -25 -9 -17 -6 -7 10 9 20 18 -30 -85 -20 -20 44 29 109 83 5 -8 -4 -7 3 -1 14 9 10 -18 -14 -22 -1 -9 37 21 5 3 -3 -4 -4 -5 0 -2 34 6 -22 -22 -20 -35 18 -8 13 20 17 0 -29 -9 -35 -4 85 113 93 -1 -171 -57 -185 -24 -10 1 -4 1 4 14 7 -5 -26 -1 -33 5 16 67 14 -13 -3 3 -4 7 5 6 6 -6 -21 -9 -8 36 48 34 4 -10 -11 7 -2 0 12 -2 4 -16 -63 11 -1 0 87 -9 6 -75 -19 12 0 -3 20 -19 3 -3 -11 3 15 -6 -6 6 -18 28 -37 28 11 -10 6 31 -6 1 2 -6 -13 4 10 -2 -11 -3 18 blend + rrcurveto + 0 -12 31 0 0 30 0 -11 0 -16 -1 9 2 21 7 -15 -6 10 -4 -8 2 -85 -6 29 -7 -16 -3 3 blend + 0 0 11 0 0 32 0 -25 0 13 1 -8 -1 -27 -6 21 6 -4 2 2 blend + rlineto + 41 -54 -21 13 -23 -10 22 5 6 -15 -14 12 19 6 -5 16 6 11 14 22 7 8 -7 1 3 0 -4 -5 -5 -7 3 5 -3 0 3 7 -6 5 -14 -13 0 7 4 -4 12 5 blend + hhcurveto + -33 -30 -16 -30 -5 15 -11 -7 2 8 -3 1 6 16 0 7 7 -8 -2 -1 -6 7 -12 -4 -2 3 5 -8 8 18 -10 6 8 -6 -9 -2 0 0 -1 0 -1 1 3 2 -1 5 blend + hvcurveto + 219 712 5 -6 -3 20 -24 -21 0 10 32 -36 -22 41 16 -24 13 -8 2 blend + rmoveto + 126 -15 20 0 12 18 -12 -16 -3 -16 -13 8 0 15 3 -8 -2 0 -4 -6 25 8 1 -6 -1 43 30 3 blend + 0 129 15 8 41 -275 -13 23 -7 -23 5 7 -19 -25 -8 0 -15 -3 8 2 0 4 -4 3 2 -5 0 3 1 -1 -21 18 19 -25 -11 15 -2 -10 19 -60 -19 34 17 -3 -8 8 5 blend + 0 -8 -41 4 -3 -2 5 0 -3 -1 1 21 -18 -19 25 11 -15 2 10 2 blend + rlineto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 200.0 + 400.0 + 900.0 + 278 + + + + + opsz + 0x0 + 8.0 + 20.0 + 60.0 + 279 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/hvar_test.py b/Tests/varLib/hvar_test.py new file mode 100644 index 0000000000..c4887ea69c --- /dev/null +++ b/Tests/varLib/hvar_test.py @@ -0,0 +1,25 @@ +from fontTools.ttLib import TTFont +from fontTools.varLib.hvar import add_HVAR +from io import StringIO +import os +import unittest +import pytest + + +def test_roundtrip(): + CWD = os.path.abspath(os.path.dirname(__file__)) + DATADIR = os.path.join(CWD, "data") + ttx_path = os.path.join(DATADIR, "MutatorSans_All_Variable.ttx") + font = TTFont() + font.importXML(ttx_path) + + HVAR1 = StringIO() + font.saveXML(HVAR1, tables=["HVAR"]) + + del font["HVAR"] + add_HVAR(font) + + HVAR2 = StringIO() + font.saveXML(HVAR2, tables=["HVAR"]) + + assert HVAR1.getvalue() == HVAR2.getvalue() diff --git a/Tests/varLib/instancer/data/3634-VF.ttx b/Tests/varLib/instancer/data/3634-VF.ttx new file mode 100644 index 0000000000..1ef750b337 --- /dev/null +++ b/Tests/varLib/instancer/data/3634-VF.ttx @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + New Font + + + Regular + + + 0.000;NONE;NewFont-Regular + + + New Font Regular + + + Version 0.000 + + + NewFont-Regular + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 400.0 + 400.0 + 900.0 + 256 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/instancer/data/test_results/3634-VF-partial.ttx b/Tests/varLib/instancer/data/test_results/3634-VF-partial.ttx new file mode 100644 index 0000000000..b639a3d6e1 --- /dev/null +++ b/Tests/varLib/instancer/data/test_results/3634-VF-partial.ttx @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Weight + + + New Font + + + Regular + + + 0.000;NONE;NewFont-Regular + + + New Font Regular + + + Version 0.000 + + + NewFont-Regular + + + Weight + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + wght + 0x0 + 401.0 + 401.0 + 900.0 + 256 + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/varLib/instancer/instancer_test.py b/Tests/varLib/instancer/instancer_test.py index b9a45058b9..1ac9f35fa8 100644 --- a/Tests/varLib/instancer/instancer_test.py +++ b/Tests/varLib/instancer/instancer_test.py @@ -2390,3 +2390,41 @@ def test_set_ribbi_bits(): assert name_id_2 == "Italic", location assert mac_style == 0b10, location assert fs_selection == 0b0000001, location + + +def test_rounds_before_iup(): + """Regression test for fonttools/fonttools#3634, with TTX based on + reproduction process there.""" + + varfont = ttLib.TTFont() + varfont.importXML(os.path.join(TESTDATA, "3634-VF.ttx")) + + # Instantiate at a new default position, sufficient to cause differences + # when unrounded but not when rounded. + partial = instancer.instantiateVariableFont(varfont, {"wght": (401, 401, 900)}) + + # Save and reload actual result to recalculate bounding box values, etc. + bytes_out = BytesIO() + partial.save(bytes_out) + bytes_out.seek(0) + partial = ttLib.TTFont(bytes_out) + + # Load expected result, then save and reload to normalise TTX output. + expected = ttLib.TTFont() + expected.importXML(os.path.join(TESTDATA, "test_results", "3634-VF-partial.ttx")) + + bytes_out = BytesIO() + expected.save(bytes_out) + bytes_out.seek(0) + expected = ttLib.TTFont(bytes_out) + + # Serialise actual and expected to TTX strings, and compare. + string_out = StringIO() + partial.saveXML(string_out) + partial_ttx = stripVariableItemsFromTTX(string_out.getvalue()) + + string_out = StringIO() + expected.saveXML(string_out) + expected_ttx = stripVariableItemsFromTTX(string_out.getvalue()) + + assert partial_ttx == expected_ttx diff --git a/Tests/varLib/interpolatable_test.py b/Tests/varLib/interpolatable_test.py index c27b233900..ac3038b1e6 100644 --- a/Tests/varLib/interpolatable_test.py +++ b/Tests/varLib/interpolatable_test.py @@ -94,6 +94,33 @@ def test_interpolatable_otf(self): otf_paths = self.get_file_list(self.tempdir, suffix) self.assertIsNone(interpolatable_main(otf_paths)) + def test_interpolatable_cff2(self): + suffix = ".otf" + ttx_dir = self.get_test_input("variable_ttx_interpolatable_cff2") + ttx_path = os.path.abspath(os.path.join(ttx_dir, "interpolatable-test.ttx")) + + self.temp_dir() + self.compile_font(ttx_path, suffix, self.tempdir) + + otf_path = self.get_file_list(self.tempdir, suffix)[0] + + problems = interpolatable_main([otf_path]) + print(problems) + self.assertEqual( + problems["uni0408"], + [ + { + "type": "underweight", + "contour": 0, + "master_1": "'wght=200.0 opsz=20.0'", + "master_2": "'wght=200.0 opsz=60.0'", + "master_1_idx": 2, + "master_2_idx": 3, + "tolerance": pytest.approx(0.9184032411892079), + }, + ], + ) + def test_interpolatable_ufo(self): ttx_dir = self.get_test_input("master_ufo") ufo_paths = self.get_file_list(ttx_dir, ".ufo", "TestFamily2-") diff --git a/Tests/varLib/stat_test.py b/Tests/varLib/stat_test.py index ce04423a57..f7af071a07 100644 --- a/Tests/varLib/stat_test.py +++ b/Tests/varLib/stat_test.py @@ -3,7 +3,8 @@ import pytest from fontTools.designspaceLib import DesignSpaceDocument from fontTools.designspaceLib.split import Range -from fontTools.varLib.stat import getStatAxes, getStatLocations +from fontTools.ttLib import TTFont, newTable +from fontTools.varLib.stat import buildVFStatTable, getStatAxes, getStatLocations @pytest.fixture @@ -189,3 +190,37 @@ def test_getStatLocations(datadir): "name": {"en": "Other"}, }, ] + + +@pytest.mark.parametrize( + "with_mac_names", + [ + pytest.param(True, id="with_mac_names"), + pytest.param(False, id="without_mac_names"), + ], +) +def test_buildVFStatTable(datadir, with_mac_names): + doc = DesignSpaceDocument.fromfile(datadir / "test_v5.designspace") + ttFont = TTFont() + + nameTable = newTable("name") + nameTable.names = [] + ttFont["name"] = nameTable + + if with_mac_names: + # addName adds a name string for both Macintosh and Windows platforms by default + nameTable.addName("Regular") + + buildVFStatTable(ttFont, doc, vfName="Test_WghtWdth") + + assert "STAT" in ttFont + + name_recs = ttFont["name"].names + assert len({nr.nameID for nr in name_recs}) == 15 + + # test that mac names don't get added if there weren't any before + mac_recs = [nr for nr in name_recs if nr.platformID == 1] + if with_mac_names: + assert len(mac_recs) > 1 + else: + assert len(mac_recs) == 0 diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py index 53acc16534..fa94e93546 100644 --- a/Tests/varLib/varLib_test.py +++ b/Tests/varLib/varLib_test.py @@ -339,10 +339,7 @@ def add_rclt(font, savepath): def test_varlib_gvar_explicit_delta(self): """The variable font contains a composite glyph odieresis which does not - need a gvar entry, because all its deltas are 0, but it must be added - anyway to work around an issue with macOS 10.14. - - https://github.com/fonttools/fonttools/issues/1381 + need a gvar entry. """ test_name = "BuildGvarCompositeExplicitDelta" self._run_varlib_build_test( diff --git a/Tests/varLib/varStore_test.py b/Tests/varLib/varStore_test.py index 7eb9d740b9..a89fda360b 100644 --- a/Tests/varLib/varStore_test.py +++ b/Tests/varLib/varStore_test.py @@ -1,4 +1,5 @@ import pytest +import random from io import StringIO from fontTools.misc.xmlWriter import XMLWriter from fontTools.misc.roundTools import noRound @@ -45,6 +46,17 @@ [100, 22000, 4000, 173000], ], ), + ( + [{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}], + [ + [random.randint(-128, 127) for _ in range(4)], + [random.randint(-128, 127) for _ in range(4)], + [random.randint(-128, 127) for _ in range(4)], + [random.randint(-32768, 32767) for _ in range(4)], + [random.randint(-32768, 32767) for _ in range(4)], + [random.randint(-32768, 32767) for _ in range(4)], + ], + ), ], ) def test_onlineVarStoreBuilder(locations, masterValues): @@ -53,6 +65,8 @@ def test_onlineVarStoreBuilder(locations, masterValues): builder = OnlineVarStoreBuilder(axisTags) builder.setModel(model) varIdxs = [] + # shuffle input order to ensure optimizer produces stable results + random.shuffle(masterValues) for masters in masterValues: _, varIdx = builder.storeMasters(masters) varIdxs.append(varIdx) @@ -178,6 +192,7 @@ def test_optimize(numRegions, varData, expectedNumVarData, expectedBytes): builder = OnlineVarStoreBuilder(axisTags) builder.setModel(model) + random.shuffle(varData) for data in varData: if type(data) is dict: newData = [0] * numRegions @@ -240,6 +255,7 @@ def test_quantize(quantization, expectedBytes): builder = OnlineVarStoreBuilder(axisTags) builder.setModel(model) + random.shuffle(varData) for data in varData: builder.storeMasters(data) @@ -268,7 +284,9 @@ def test_optimize_overflow(): builder = OnlineVarStoreBuilder(axisTags) builder.setModel(model) - for data in range(0, 0xFFFF * 2): + varData = list(range(0, 0xFFFF * 2)) + random.shuffle(varData) + for data in varData: data = [0, data] builder.storeMasters(data, round=noRound) 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/dev-requirements.txt b/dev-requirements.txt index 4eacf2733a..036591192e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,6 +3,7 @@ tox>=2.5 bump2version>=0.5.6 sphinx>=1.5.5 mypy>=0.782 +readme_renderer[md]>=43.0 # Pin black as each version could change formatting, breaking CI randomly. -black==24.4.2 +black==24.10.0 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 c57238030e..bcef14cd21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,18 +3,18 @@ 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.13.0; 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 +zopfli==0.2.3.post1 fs==2.4.16 -skia-pathops==0.8.0.post1; platform_python_implementation != "PyPy" +skia-pathops==0.8.0.post2; platform_python_implementation != "PyPy" # this is only required to run Tests/cu2qu/{ufo,cli}_test.py -ufoLib2==0.16.0 -ufo2ft==3.2.1 -pyobjc==10.2; sys_platform == "darwin" -freetype-py==2.4.0 -uharfbuzz==0.39.1 -glyphsLib==6.7.1 # this is only required to run Tests/varLib/interpolatable_test.py -lxml==5.2.1 -sympy==1.12 +ufoLib2==0.17.0 +ufo2ft==3.3.1 +pyobjc==10.3.2; sys_platform == "darwin" +freetype-py==2.5.1 +uharfbuzz==0.43.0 +glyphsLib==6.9.5 # this is only required to run Tests/varLib/interpolatable_test.py +lxml==5.3.0 +sympy==1.13.3 diff --git a/setup.cfg b/setup.cfg index e7df801a69..8aef1d31a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.52.2 +current_version = 4.58.1.dev0 commit = True tag = False tag_name = {new_version} diff --git a/setup.py b/setup.py index 2a22b87bd8..97b22728b4 100755 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ from glob import glob from setuptools import setup, find_packages, Command, Extension from setuptools.command.build_ext import build_ext as _build_ext +from setuptools.errors import SetupError from distutils import log from distutils.util import convert_path import subprocess as sp @@ -33,7 +34,7 @@ def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): setup_requires.append("wheel") if {"release"}.intersection(sys.argv): - setup_requires.append("bump2version") + setup_requires.extend(["bump2version", "readme_renderer"]) try: __import__("cython") @@ -157,15 +158,14 @@ 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", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3", "Topic :: Text Processing :: Fonts", "Topic :: Multimedia :: Graphics", @@ -179,8 +179,21 @@ def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): with io.open("README.rst", "r", encoding="utf-8") as readme: long_description = readme.read() long_description += "\nChangelog\n~~~~~~~~~\n\n" +# At the same time, we don't want the PyPI package description becoming too +# long (some tools like Azure DevOps impose maximum length of 324KB) so we +# trim it when we see a special rst comment +changelog_limit_re = re.compile(r"^\.\. package description limit") with io.open("NEWS.rst", "r", encoding="utf-8") as changelog: - long_description += changelog.read() + short_changelog = [] + for line in changelog: + if changelog_limit_re.match(line): + break + short_changelog.append(line) +short_changelog.append( + "\\... see `here `__ " + "for earlier changes\n" +) +long_description += "".join(short_changelog) @contextlib.contextmanager @@ -266,7 +279,19 @@ def finalize_options(self): raise DistutilsOptionError("--major/--minor are mutually exclusive") self.part = "major" if self.major else "minor" if self.minor else None + def check_long_description_syntax(self): + import readme_renderer.rst + + result = readme_renderer.rst.render(long_description, stream=sys.stderr) + if result is None: + raise SetupError( + "`long_description` has syntax errors in markup" + " and would not be rendered on PyPI." + ) + def run(self): + self.check_long_description_syntax() + if self.part is not None: log.info("bumping '%s' version" % self.part) self.bumpversion(self.part, commit=False) @@ -468,7 +493,7 @@ def build_extensions(self): setup_params = dict( name="fonttools", - version="4.52.2", + version="4.58.1.dev0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com", @@ -476,9 +501,11 @@ 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"}, packages=find_packages("Lib"), include_package_data=True, diff --git a/tox.ini b/tox.ini index 9a6fce0fa8..b9364c0841 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.0 -envlist = lint, py3{8,9,10,11,12}-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