From 713771ba85fc76eb48983fb08ac51e180a55825b Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Thu, 16 Dec 2021 10:43:35 +0100 Subject: [PATCH 001/105] Derive number of entries to decompile from data length --- Lib/fontTools/ttLib/tables/T_S_I__0.py | 4 ++-- Lib/fontTools/ttLib/tables/T_S_I__5.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/T_S_I__0.py b/Lib/fontTools/ttLib/tables/T_S_I__0.py index b187f42510..e4e6a32af9 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__0.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__0.py @@ -19,10 +19,10 @@ class table_T_S_I__0(DefaultTable.DefaultTable): dependencies = ["TSI1"] def decompile(self, data, ttFont): - numGlyphs = ttFont['maxp'].numGlyphs indices = [] size = struct.calcsize(tsi0Format) - for i in range(numGlyphs + 5): + numEntries = len(data) // size + for i in range(numEntries): glyphID, textLength, textOffset = fixlongs(*struct.unpack(tsi0Format, data[:size])) indices.append((glyphID, textLength, textOffset)) data = data[size:] diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py index 7be09f9a4e..1f7f63b192 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__5.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py @@ -12,13 +12,11 @@ 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): + for i in range(len(data) // 2): self.glyphGrouping[ttFont.getGlyphName(i)] = a[i] def compile(self, ttFont): From 820ba9d386a2fb6d6eb4b36b1c430848316b6faa Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Wed, 16 Nov 2022 18:49:03 +0000 Subject: [PATCH 002/105] Clarify user expectations for getStatNames Closes #2890 --- Lib/fontTools/designspaceLib/statNames.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/fontTools/designspaceLib/statNames.py b/Lib/fontTools/designspaceLib/statNames.py index a164169da6..3b8ab2f887 100644 --- a/Lib/fontTools/designspaceLib/statNames.py +++ b/Lib/fontTools/designspaceLib/statNames.py @@ -60,6 +60,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] = {} From 0f920443e964f655b74c92ca1ca8f3afe7c6f5b0 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 22 Nov 2022 16:25:53 +0000 Subject: [PATCH 003/105] Make some instance attributes optional --- Doc/source/designspaceLib/xml.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/source/designspaceLib/xml.rst b/Doc/source/designspaceLib/xml.rst index ee4edef1ca..2aadc2dde4 100644 --- a/Doc/source/designspaceLib/xml.rst +++ b/Doc/source/designspaceLib/xml.rst @@ -814,12 +814,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. From 858a12c23ebebd72d9c71b4d8b01479622c76439 Mon Sep 17 00:00:00 2001 From: OMKAR MAKHARE Date: Mon, 16 Oct 2023 19:45:48 +0530 Subject: [PATCH 004/105] Remove typo --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index bcb7f0d47f..d4618daf74 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ What is this? fonts to and from an XML text format, which is also called TTX. It supports TrueType, OpenType, AFM and to an extent Type 1 and some Mac-specific formats. The project has an `MIT open-source - licence `__. + license `__. | Among other things this means you can use it free of charge. `User documentation `_ and @@ -247,7 +247,7 @@ How to make a new release automate that too. -Acknowledgements +Acknowledgments ~~~~~~~~~~~~~~~~ In alphabetical order: From 1b819fc1d4dcf61d10ad907225cb9ee23b2570f7 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Fri, 6 Dec 2024 15:54:12 +0000 Subject: [PATCH 005/105] [Docs] Add to ufoLib documentation --- Doc/source/ufoLib/converters.rst | 2 +- Doc/source/ufoLib/errors.rst | 1 - Doc/source/ufoLib/etree.rst | 9 + Doc/source/ufoLib/filenames.rst | 1 - Doc/source/ufoLib/glifLib.rst | 1 - Doc/source/ufoLib/index.rst | 20 ++- Doc/source/ufoLib/kerning.rst | 1 - Doc/source/ufoLib/plistlib.rst | 1 - Doc/source/ufoLib/pointpen.rst | 1 - Doc/source/ufoLib/utils.rst | 7 +- Doc/source/ufoLib/validators.rst | 1 - Lib/fontTools/ufoLib/__init__.py | 4 +- Lib/fontTools/ufoLib/converters.py | 114 ++++++++++--- Lib/fontTools/ufoLib/errors.py | 8 + Lib/fontTools/ufoLib/etree.py | 2 +- Lib/fontTools/ufoLib/filenames.py | 255 ++++++++++++++++++----------- Lib/fontTools/ufoLib/glifLib.py | 11 +- Lib/fontTools/ufoLib/kerning.py | 102 ++++++++---- Lib/fontTools/ufoLib/utils.py | 7 +- 19 files changed, 360 insertions(+), 188 deletions(-) create mode 100644 Doc/source/ufoLib/etree.rst diff --git a/Doc/source/ufoLib/converters.rst b/Doc/source/ufoLib/converters.rst index 0202d43358..134be52d09 100644 --- a/Doc/source/ufoLib/converters.rst +++ b/Doc/source/ufoLib/converters.rst @@ -3,6 +3,6 @@ converters: Conversion functions for kerning and groups ####################################################### .. automodule:: fontTools.ufoLib.converters - :inherited-members: + :no-inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/errors.rst b/Doc/source/ufoLib/errors.rst index 179e968ee1..1896ce516d 100644 --- a/Doc/source/ufoLib/errors.rst +++ b/Doc/source/ufoLib/errors.rst @@ -3,6 +3,5 @@ errors: Exceptions for handling UFO-specific errors ################################################### .. automodule:: fontTools.ufoLib.errors - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/etree.rst b/Doc/source/ufoLib/etree.rst new file mode 100644 index 0000000000..bd43f57254 --- /dev/null +++ b/Doc/source/ufoLib/etree.rst @@ -0,0 +1,9 @@ +############################################################# +etree: Shim module for the ElementTree XML API *[deprecated]* +############################################################# + +.. important:: + + .. automodule:: fontTools.ufoLib.etree + :members: + :undoc-members: diff --git a/Doc/source/ufoLib/filenames.rst b/Doc/source/ufoLib/filenames.rst index d3eef04966..41ab4a0c27 100644 --- a/Doc/source/ufoLib/filenames.rst +++ b/Doc/source/ufoLib/filenames.rst @@ -3,6 +3,5 @@ filenames: Functions to convert between file names and user-facing strings ########################################################################## .. automodule:: fontTools.ufoLib.filenames - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/glifLib.rst b/Doc/source/ufoLib/glifLib.rst index 9213260bc4..4e48e75f1b 100644 --- a/Doc/source/ufoLib/glifLib.rst +++ b/Doc/source/ufoLib/glifLib.rst @@ -3,6 +3,5 @@ glifLib: Read and write UFO .glif files ####################################### .. automodule:: fontTools.ufoLib.glifLib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/index.rst b/Doc/source/ufoLib/index.rst index a865fd2f65..2bc5ab2a0d 100644 --- a/Doc/source/ufoLib/index.rst +++ b/Doc/source/ufoLib/index.rst @@ -3,18 +3,18 @@ ufoLib: Read and write Unified Font Object files ################################################ .. rubric:: Overview: - :heading-level: 3 + :heading-level: 2 .. automodule:: fontTools.ufoLib - :inherited-members: + :no-inherited-members: :members: :undoc-members: :member-order: bysource - .. rubric:: Submodules: - :heading-level: 3 + .. rubric:: Modules: + :heading-level: 2 - ufoLib provides the following submodules: + ufoLib provides the following modules: .. toctree:: :maxdepth: 1 @@ -28,16 +28,20 @@ ufoLib: Read and write Unified Font Object files utils validators - Two deprecated submodules are also currently included: + Three deprecated modules are also currently included: .. toctree:: :maxdepth: 1 - + + etree plistlib pointpen + Note also that :mod:`ufoLib` supports some :doc:`optional ` + external libraries. + .. rubric:: Module members: - :heading-level: 3 + :heading-level: 2 .. autodata:: fontInfoAttributesVersion1 .. autodata:: fontInfoAttributesVersion2 diff --git a/Doc/source/ufoLib/kerning.rst b/Doc/source/ufoLib/kerning.rst index b8f1c38c5d..e16f993822 100644 --- a/Doc/source/ufoLib/kerning.rst +++ b/Doc/source/ufoLib/kerning.rst @@ -3,6 +3,5 @@ kerning: Support for accessing kerning data ########################################### .. automodule:: fontTools.ufoLib.kerning - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/plistlib.rst b/Doc/source/ufoLib/plistlib.rst index 6ae1a0ce1d..eb919dce4d 100644 --- a/Doc/source/ufoLib/plistlib.rst +++ b/Doc/source/ufoLib/plistlib.rst @@ -5,6 +5,5 @@ plistlib: Support for reading and writing .plist files *[deprecated]* .. important:: .. automodule:: fontTools.ufoLib.plistlib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/pointpen.rst b/Doc/source/ufoLib/pointpen.rst index d71a727265..0f11bcce0f 100644 --- a/Doc/source/ufoLib/pointpen.rst +++ b/Doc/source/ufoLib/pointpen.rst @@ -5,6 +5,5 @@ pointPen: A pen for accessing points in a glyph contour *[deprecated]* .. important:: .. automodule:: fontTools.ufoLib.pointPen - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/utils.rst b/Doc/source/ufoLib/utils.rst index bef7828152..2a63e3812a 100644 --- a/Doc/source/ufoLib/utils.rst +++ b/Doc/source/ufoLib/utils.rst @@ -1,8 +1,7 @@ -##################################### -utils: Miscellaneous helper functions -##################################### +######################################### +utils: Miscellaneous UFO helper functions +######################################### .. automodule:: fontTools.ufoLib.utils - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/ufoLib/validators.rst b/Doc/source/ufoLib/validators.rst index c512e873f8..2288378229 100644 --- a/Doc/source/ufoLib/validators.rst +++ b/Doc/source/ufoLib/validators.rst @@ -3,6 +3,5 @@ validators: Data-validation functions ##################################### .. automodule:: fontTools.ufoLib.validators - :inherited-members: :members: :undoc-members: diff --git a/Lib/fontTools/ufoLib/__init__.py b/Lib/fontTools/ufoLib/__init__.py index 42c06734ea..f76938a8f1 100755 --- a/Lib/fontTools/ufoLib/__init__.py +++ b/Lib/fontTools/ufoLib/__init__.py @@ -204,7 +204,7 @@ class UFOReader(_UFOBaseIO): """Read the various components of a .ufo. Attributes: - path: An `os.PathLike` object pointing to the .ufo. + path: An :class:`os.PathLike` object pointing to the .ufo. validate: A boolean indicating if the data read should be validated. Defaults to `True`. @@ -891,7 +891,7 @@ class UFOWriter(UFOReader): """Write the various components of a .ufo. Attributes: - path: An `os.PathLike` object pointing to the .ufo. + path: An :class:`os.PathLike` object pointing to the .ufo. formatVersion: the UFO format version as a tuple of integers (major, minor), or as a single integer for the major digit only (minor is implied to be 0). By default, the latest formatVersion will be used; currently it is 3.0, diff --git a/Lib/fontTools/ufoLib/converters.py b/Lib/fontTools/ufoLib/converters.py index 88a26c616a..4ee6b05e33 100644 --- a/Lib/fontTools/ufoLib/converters.py +++ b/Lib/fontTools/ufoLib/converters.py @@ -1,11 +1,33 @@ """ -Conversion functions. +Functions for converting UFO1 or UFO2 files into UFO3 format. + +Currently provides functionality for converting kerning rules +and kerning groups. Conversion is only supported _from_ UFO1 +or UFO2, and _to_ UFO3. """ # adapted from the UFO spec def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups, glyphSet=()): + """Convert kerning data in UFO1 or UFO2 syntax into UFO3 syntax. + + Args: + kerning: + A dictionary containing the kerning rules defined in + the UFO font, as used in :class:`.UFOReader` objects. + groups: + A dictionary containing the groups defined in the UFO + font, as used in :class:`.UFOReader` objects. + glyphSet: + Optional; a set of glyph objects to skip (default: None). + + Returns: + 1. A dictionary representing the converted kerning data. + 2. A copy of the groups dictionary, with all groups renamed to UFO3 syntax. + 3. A dictionary containing the mapping of old group names to new group names. + + """ # gather known kerning groups based on the prefixes firstReferencedGroups, secondReferencedGroups = findKnownKerningGroups(groups) # Make lists of groups referenced in kerning pairs. @@ -63,35 +85,54 @@ def convertUFO1OrUFO2KerningToUFO3Kerning(kerning, groups, glyphSet=()): def findKnownKerningGroups(groups): - """ - This will find kerning groups with known prefixes. - In some cases not all kerning groups will be referenced - by the kerning pairs. The algorithm for locating groups - in convertUFO1OrUFO2KerningToUFO3Kerning will miss these - unreferenced groups. By scanning for known prefixes + """Find all kerning groups in a UFO1 or UFO2 font that use known prefixes. + + In some cases, not all kerning groups will be referenced + by the kerning pairs in a UFO. The algorithm for locating + groups in :func:`convertUFO1OrUFO2KerningToUFO3Kerning` will + miss these unreferenced groups. By scanning for known prefixes, this function will catch all of the prefixed groups. - These are the prefixes and sides that are handled: + The prefixes and sides by this function are: + @MMK_L_ - side 1 @MMK_R_ - side 2 - >>> testGroups = { - ... "@MMK_L_1" : None, - ... "@MMK_L_2" : None, - ... "@MMK_L_3" : None, - ... "@MMK_R_1" : None, - ... "@MMK_R_2" : None, - ... "@MMK_R_3" : None, - ... "@MMK_l_1" : None, - ... "@MMK_r_1" : None, - ... "@MMK_X_1" : None, - ... "foo" : None, - ... } - >>> first, second = findKnownKerningGroups(testGroups) - >>> sorted(first) == ['@MMK_L_1', '@MMK_L_2', '@MMK_L_3'] - True - >>> sorted(second) == ['@MMK_R_1', '@MMK_R_2', '@MMK_R_3'] - True + as defined in the UFO1 specification. + + Args: + groups: + A dictionary containing the groups defined in the UFO + font, as read by :class:`.UFOReader`. + + Returns: + Two sets; the first containing the names of all + first-side kerning groups identified in the ``groups`` + dictionary, and the second containing the names of all + second-side kerning groups identified. + + "First-side" and "second-side" are with respect to the + writing direction of the script. + + Example:: + + >>> testGroups = { + ... "@MMK_L_1" : None, + ... "@MMK_L_2" : None, + ... "@MMK_L_3" : None, + ... "@MMK_R_1" : None, + ... "@MMK_R_2" : None, + ... "@MMK_R_3" : None, + ... "@MMK_l_1" : None, + ... "@MMK_r_1" : None, + ... "@MMK_X_1" : None, + ... "foo" : None, + ... } + >>> first, second = findKnownKerningGroups(testGroups) + >>> sorted(first) == ['@MMK_L_1', '@MMK_L_2', '@MMK_L_3'] + True + >>> sorted(second) == ['@MMK_R_1', '@MMK_R_2', '@MMK_R_3'] + True """ knownFirstGroupPrefixes = ["@MMK_L_"] knownSecondGroupPrefixes = ["@MMK_R_"] @@ -110,6 +151,27 @@ def findKnownKerningGroups(groups): def makeUniqueGroupName(name, groupNames, counter=0): + """Make a kerning group name that will be unique within the set of group names. + + If the requested kerning group name already exists within the set, this + will return a new name by adding an incremented counter to the end + of the requested name. + + Args: + name: + The requested kerning group name. + groupNames: + A list of the existing kerning group names. + counter: + Optional; a counter of group names already seen (default: 0). If + :attr:`.counter` is not provided, the function will recurse, + incrementing the value of :attr:`.counter` until it finds the + first unused ``name+counter`` combination, and return that result. + + Returns: + A unique kerning group name composed of the requested name suffixed + by the smallest available integer counter. + """ # Add a number to the name if the counter is higher than zero. newName = name if counter > 0: @@ -123,6 +185,8 @@ def makeUniqueGroupName(name, groupNames, counter=0): def test(): """ + Tests for :func:`.convertUFO1OrUFO2KerningToUFO3Kerning`. + No known prefixes. >>> testKerning = { diff --git a/Lib/fontTools/ufoLib/errors.py b/Lib/fontTools/ufoLib/errors.py index e05dd438b4..6cc9fec399 100644 --- a/Lib/fontTools/ufoLib/errors.py +++ b/Lib/fontTools/ufoLib/errors.py @@ -10,6 +10,14 @@ class UnsupportedUFOFormat(UFOLibError): class GlifLibError(UFOLibError): + """An error raised by glifLib. + + This class is a loose backport of PEP 678, adding a :attr:`.note` + attribute that can hold additional context for errors encountered. + + It will be maintained until only Python 3.11-and-later are supported. + """ + def _add_note(self, note: str) -> None: # Loose backport of PEP 678 until we only support Python 3.11+, used for # adding additional context to errors. diff --git a/Lib/fontTools/ufoLib/etree.py b/Lib/fontTools/ufoLib/etree.py index 77e3c16e2b..07b924ac85 100644 --- a/Lib/fontTools/ufoLib/etree.py +++ b/Lib/fontTools/ufoLib/etree.py @@ -1,5 +1,5 @@ """DEPRECATED - This module is kept here only as a backward compatibility shim -for the old ufoLib.etree module, which was moved to fontTools.misc.etree. +for the old ufoLib.etree module, which was moved to :mod:`fontTools.misc.etree`. Please use the latter instead. """ diff --git a/Lib/fontTools/ufoLib/filenames.py b/Lib/fontTools/ufoLib/filenames.py index 7f1af58ee8..83442f1c8c 100644 --- a/Lib/fontTools/ufoLib/filenames.py +++ b/Lib/fontTools/ufoLib/filenames.py @@ -1,6 +1,22 @@ """ -User name to file name conversion. -This was taken from the UFO 3 spec. +Convert user-provided internal UFO names to spec-compliant filenames. + +This module implements the algorithm for converting between a "user name" - +something that a user can choose arbitrarily inside a font editor - and a file +name suitable for use in a wide range of operating systems and filesystems. + +The `UFO 3 specification `_ +provides an example of an algorithm for such conversion, which avoids illegal +characters, reserved file names, ambiguity between upper- and lower-case +characters, and clashes with existing files. + +This code was originally copied from +`ufoLib `_ +by Tal Leming and is copyright (c) 2005-2016, The RoboFab Developers: + +- Erik van Blokland +- Tal Leming +- Just van Rossum """ # Restrictions are taken mostly from @@ -93,53 +109,69 @@ class NameTranslationError(Exception): def userNameToFileName(userName: str, existing=(), prefix="", suffix=""): - """ - `existing` should be a set-like object. - - >>> userNameToFileName("a") == "a" - True - >>> userNameToFileName("A") == "A_" - True - >>> userNameToFileName("AE") == "A_E_" - True - >>> userNameToFileName("Ae") == "A_e" - True - >>> userNameToFileName("ae") == "ae" - True - >>> userNameToFileName("aE") == "aE_" - True - >>> userNameToFileName("a.alt") == "a.alt" - True - >>> userNameToFileName("A.alt") == "A_.alt" - True - >>> userNameToFileName("A.Alt") == "A_.A_lt" - True - >>> userNameToFileName("A.aLt") == "A_.aL_t" - True - >>> userNameToFileName(u"A.alT") == "A_.alT_" - True - >>> userNameToFileName("T_H") == "T__H_" - True - >>> userNameToFileName("T_h") == "T__h" - True - >>> userNameToFileName("t_h") == "t_h" - True - >>> userNameToFileName("F_F_I") == "F__F__I_" - True - >>> userNameToFileName("f_f_i") == "f_f_i" - True - >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" - True - >>> userNameToFileName(".notdef") == "_notdef" - True - >>> userNameToFileName("con") == "_con" - True - >>> userNameToFileName("CON") == "C_O_N_" - True - >>> userNameToFileName("con.alt") == "_con.alt" - True - >>> userNameToFileName("alt.con") == "alt._con" - True + """Converts from a user name to a file name. + + Takes care to avoid illegal characters, reserved file names, ambiguity between + upper- and lower-case characters, and clashes with existing files. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + Raises: + NameTranslationError: If no suitable name could be generated. + + Examples:: + + >>> userNameToFileName("a") == "a" + True + >>> userNameToFileName("A") == "A_" + True + >>> userNameToFileName("AE") == "A_E_" + True + >>> userNameToFileName("Ae") == "A_e" + True + >>> userNameToFileName("ae") == "ae" + True + >>> userNameToFileName("aE") == "aE_" + True + >>> userNameToFileName("a.alt") == "a.alt" + True + >>> userNameToFileName("A.alt") == "A_.alt" + True + >>> userNameToFileName("A.Alt") == "A_.A_lt" + True + >>> userNameToFileName("A.aLt") == "A_.aL_t" + True + >>> userNameToFileName(u"A.alT") == "A_.alT_" + True + >>> userNameToFileName("T_H") == "T__H_" + True + >>> userNameToFileName("T_h") == "T__h" + True + >>> userNameToFileName("t_h") == "t_h" + True + >>> userNameToFileName("F_F_I") == "F__F__I_" + True + >>> userNameToFileName("f_f_i") == "f_f_i" + True + >>> userNameToFileName("Aacute_V.swash") == "A_acute_V_.swash" + True + >>> userNameToFileName(".notdef") == "_notdef" + True + >>> userNameToFileName("con") == "_con" + True + >>> userNameToFileName("CON") == "C_O_N_" + True + >>> userNameToFileName("con.alt") == "_con.alt" + True + >>> userNameToFileName("alt.con") == "alt._con" + True """ # the incoming name must be a string if not isinstance(userName, str): @@ -181,33 +213,42 @@ def userNameToFileName(userName: str, existing=(), prefix="", suffix=""): def handleClash1(userName, existing=[], prefix="", suffix=""): - """ - existing should be a case-insensitive list - of all existing file names. - - >>> prefix = ("0" * 5) + "." - >>> suffix = "." + ("0" * 10) - >>> existing = ["a" * 5] - - >>> e = list(existing) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000001.0000000000') - True - - >>> e = list(existing) - >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000002.0000000000') - True - - >>> e = list(existing) - >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) - >>> handleClash1(userName="A" * 5, existing=e, - ... prefix=prefix, suffix=suffix) == ( - ... '00000.AAAAA000000000000001.0000000000') - True + """A helper function that resolves collisions with existing names when choosing a filename. + + This function attempts to append an unused integer counter to the filename. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = ["a" * 5] + + >>> e = list(existing) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "aaaaa" + "1".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000002.0000000000') + True + + >>> e = list(existing) + >>> e.append(prefix + "AAAAA" + "2".zfill(15) + suffix) + >>> handleClash1(userName="A" * 5, existing=e, + ... prefix=prefix, suffix=suffix) == ( + ... '00000.AAAAA000000000000001.0000000000') + True """ # if the prefix length + user name length + suffix length + 15 is at # or past the maximum length, silce 15 characters off of the user name @@ -238,30 +279,44 @@ def handleClash1(userName, existing=[], prefix="", suffix=""): def handleClash2(existing=[], prefix="", suffix=""): - """ - existing should be a case-insensitive list - of all existing file names. - - >>> prefix = ("0" * 5) + "." - >>> suffix = "." + ("0" * 10) - >>> existing = [prefix + str(i) + suffix for i in range(100)] - - >>> e = list(existing) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.100.0000000000') - True - - >>> e = list(existing) - >>> e.remove(prefix + "1" + suffix) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.1.0000000000') - True - - >>> e = list(existing) - >>> e.remove(prefix + "2" + suffix) - >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( - ... '00000.2.0000000000') - True + """A helper function that resolves collisions with existing names when choosing a filename. + + This function is a fallback to :func:`handleClash1`. It attempts to append an unused integer counter to the filename. + + Args: + userName (str): The input file name. + existing: A case-insensitive list of all existing file names. + prefix: Prefix to be prepended to the file name. + suffix: Suffix to be appended to the file name. + + Returns: + A suitable filename. + + Raises: + NameTranslationError: If no suitable name could be generated. + + Examples:: + + >>> prefix = ("0" * 5) + "." + >>> suffix = "." + ("0" * 10) + >>> existing = [prefix + str(i) + suffix for i in range(100)] + + >>> e = list(existing) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.100.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "1" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.1.0000000000') + True + + >>> e = list(existing) + >>> e.remove(prefix + "2" + suffix) + >>> handleClash2(existing=e, prefix=prefix, suffix=suffix) == ( + ... '00000.2.0000000000') + True """ # calculate the longest possible string maxLength = maxFileNameLength - len(prefix) - len(suffix) diff --git a/Lib/fontTools/ufoLib/glifLib.py b/Lib/fontTools/ufoLib/glifLib.py index abbda49146..a5a05003ee 100755 --- a/Lib/fontTools/ufoLib/glifLib.py +++ b/Lib/fontTools/ufoLib/glifLib.py @@ -1,11 +1,11 @@ """ -glifLib.py -- Generic module for reading and writing the .glif format. +Generic module for reading and writing the .glif format. More info about the .glif format (GLyphInterchangeFormat) can be found here: http://unifiedfontobject.org -The main class in this module is GlyphSet. It manages a set of .glif files +The main class in this module is :class:`GlyphSet`. It manages a set of .glif files in a folder. It offers two ways to read glyph data, and one way to write glyph data. See the class doc string for details. """ @@ -60,6 +60,13 @@ class GLIFFormatVersion(tuple, _VersionTupleEnumMixin, enum.Enum): + """Class representing the versions of the .glif format supported by the UFO version in use. + + For a given :mod:`fontTools.ufoLib.UFOFormatVersion`, the :func:`supported_versions` method will + return the supported versions of the GLIF file format. If the UFO version is unspecified, the + :func:`supported_versions` method will return all available GLIF format versions. + """ + FORMAT_1_0 = (1, 0) FORMAT_2_0 = (2, 0) diff --git a/Lib/fontTools/ufoLib/kerning.py b/Lib/fontTools/ufoLib/kerning.py index 8a1dca5b68..5c84dd720a 100644 --- a/Lib/fontTools/ufoLib/kerning.py +++ b/Lib/fontTools/ufoLib/kerning.py @@ -1,43 +1,73 @@ def lookupKerningValue( pair, kerning, groups, fallback=0, glyphToFirstGroup=None, glyphToSecondGroup=None ): - """ - Note: This expects kerning to be a flat dictionary - of kerning pairs, not the nested structure used - in kerning.plist. + """Retrieve the kerning value (if any) between a pair of elements. + + The elments can be either individual glyphs (by name) or kerning + groups (by name), or any combination of the two. + + Args: + pair: + A tuple, in logical order (first, second) with respect + to the reading direction, to query the font for kerning + information on. Each element in the tuple can be either + a glyph name or a kerning group name. + kerning: + A dictionary of kerning pairs. + groups: + A set of kerning groups. + fallback: + The fallback value to return if no kern is found between + the elements in ``pair``. Defaults to 0. + glyphToFirstGroup: + A dictionary mapping glyph names to the first-glyph kerning + groups to which they belong. Defaults to ``None``. + glyphToSecondGroup: + A dictionary mapping glyph names to the second-glyph kerning + groups to which they belong. Defaults to ``None``. + + Returns: + The kerning value between the element pair. If no kerning for + the pair is found, the fallback value is returned. + + Note: This function expects the ``kerning`` argument to be a flat + dictionary of kerning pairs, not the nested structure used in a + kerning.plist file. + + Examples:: - >>> groups = { - ... "public.kern1.O" : ["O", "D", "Q"], - ... "public.kern2.E" : ["E", "F"] - ... } - >>> kerning = { - ... ("public.kern1.O", "public.kern2.E") : -100, - ... ("public.kern1.O", "F") : -200, - ... ("D", "F") : -300 - ... } - >>> lookupKerningValue(("D", "F"), kerning, groups) - -300 - >>> lookupKerningValue(("O", "F"), kerning, groups) - -200 - >>> lookupKerningValue(("O", "E"), kerning, groups) - -100 - >>> lookupKerningValue(("O", "O"), kerning, groups) - 0 - >>> lookupKerningValue(("E", "E"), kerning, groups) - 0 - >>> lookupKerningValue(("E", "O"), kerning, groups) - 0 - >>> lookupKerningValue(("X", "X"), kerning, groups) - 0 - >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"), - ... kerning, groups) - -100 - >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups) - -200 - >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups) - -100 - >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups) - 0 + >>> groups = { + ... "public.kern1.O" : ["O", "D", "Q"], + ... "public.kern2.E" : ["E", "F"] + ... } + >>> kerning = { + ... ("public.kern1.O", "public.kern2.E") : -100, + ... ("public.kern1.O", "F") : -200, + ... ("D", "F") : -300 + ... } + >>> lookupKerningValue(("D", "F"), kerning, groups) + -300 + >>> lookupKerningValue(("O", "F"), kerning, groups) + -200 + >>> lookupKerningValue(("O", "E"), kerning, groups) + -100 + >>> lookupKerningValue(("O", "O"), kerning, groups) + 0 + >>> lookupKerningValue(("E", "E"), kerning, groups) + 0 + >>> lookupKerningValue(("E", "O"), kerning, groups) + 0 + >>> lookupKerningValue(("X", "X"), kerning, groups) + 0 + >>> lookupKerningValue(("public.kern1.O", "public.kern2.E"), + ... kerning, groups) + -100 + >>> lookupKerningValue(("public.kern1.O", "F"), kerning, groups) + -200 + >>> lookupKerningValue(("O", "public.kern2.E"), kerning, groups) + -100 + >>> lookupKerningValue(("public.kern1.X", "public.kern2.X"), kerning, groups) + 0 """ # quickly check to see if the pair is in the kerning dictionary if pair in kerning: diff --git a/Lib/fontTools/ufoLib/utils.py b/Lib/fontTools/ufoLib/utils.py index 45ec1c564b..45ae5e812e 100644 --- a/Lib/fontTools/ufoLib/utils.py +++ b/Lib/fontTools/ufoLib/utils.py @@ -1,5 +1,8 @@ -"""The module contains miscellaneous helpers. -It's not considered part of the public ufoLib API. +"""This module contains miscellaneous helpers. + +It is not considered part of the public ufoLib API. It does, however, +define the :py:obj:`.deprecated` decorator that is used elsewhere in +the module. """ import warnings From 8114dd69947efdabc49813359f42268ee55b36d5 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Fri, 6 Dec 2024 15:58:02 +0000 Subject: [PATCH 006/105] Add cairoPen and quartzPen doc pages --- Doc/source/pens/cairoPen.rst | 7 +++++++ Doc/source/pens/quartzPen.rst | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 Doc/source/pens/cairoPen.rst create mode 100644 Doc/source/pens/quartzPen.rst 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/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: From 73e282c6eeb7ce050fece74ed13cea85b3308a78 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Fri, 6 Dec 2024 15:59:12 +0000 Subject: [PATCH 007/105] Regularize pen doc page structure. --- Doc/source/pens/areaPen.rst | 1 - Doc/source/pens/basePen.rst | 1 - Doc/source/pens/boundsPen.rst | 1 - Doc/source/pens/cocoaPen.rst | 4 +++- Doc/source/pens/cu2quPen.rst | 1 - Doc/source/pens/filterPen.rst | 1 - Doc/source/pens/freetypePen.rst | 1 - Doc/source/pens/momentsPen.rst | 1 - Doc/source/pens/perimeterPen.rst | 1 - Doc/source/pens/pointInsidePen.rst | 1 - Doc/source/pens/pointPen.rst | 1 - Doc/source/pens/qtPen.rst | 5 ++++- Doc/source/pens/recordingPen.rst | 1 - Doc/source/pens/reportLabPen.rst | 5 ++++- Doc/source/pens/reverseContourPen.rst | 1 - Doc/source/pens/roundingPen.rst | 1 - Doc/source/pens/statisticsPen.rst | 1 - Doc/source/pens/svgPathPen.rst | 1 - Doc/source/pens/t2CharStringPen.rst | 1 - Doc/source/pens/teePen.rst | 1 - Doc/source/pens/transformPen.rst | 1 - Doc/source/pens/ttGlyphPen.rst | 1 - Doc/source/pens/wxPen.rst | 1 - 23 files changed, 11 insertions(+), 23 deletions(-) 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..f5965b16c7 100644 --- a/Doc/source/pens/basePen.rst +++ b/Doc/source/pens/basePen.rst @@ -3,6 +3,5 @@ basePen ####### .. 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/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/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..8c7026e376 100644 --- a/Doc/source/pens/pointPen.rst +++ b/Doc/source/pens/pointPen.rst @@ -3,6 +3,5 @@ pointPen ######## .. 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/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: From dcdb1d43109a33209a6293366883eae17f0419eb Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Fri, 6 Dec 2024 15:59:44 +0000 Subject: [PATCH 008/105] Update pens section landing page. --- Doc/source/pens/index.rst | 65 ++++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 11 deletions(-) diff --git a/Doc/source/pens/index.rst b/Doc/source/pens/index.rst index e26d6d5b20..d85d5c0727 100644 --- a/Doc/source/pens/index.rst +++ b/Doc/source/pens/index.rst @@ -2,8 +2,12 @@ pens: Inspect and manipulate glyph outlines ########################################### +.. contents:: On this page: + :local: + + .. rubric:: Overview: - :heading-level: 3 + :heading-level: 2 The fontTools **pens** are a collection of classes that can operate on a font glyph via the points and the contours of the glyph's outlines. @@ -15,30 +19,59 @@ 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. +The majority of the pens are segment-oriented, meaning that they +operate by processing the Bezier segments of each glyph or glyph +component in order. This model corresponds to the way that glyph data +is stored in both TrueType-flavored and CFF-flavored OpenType +fonts. + +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. UFO's +point-only file format can be deterministically converted to +segment-oriented form and vice-versa; therefore all pens are available +to be used with UFO sources. The generic example can be found on the +:ref:`pointPen` page: + +.. toctree:: + :maxdepth: 1 + + pointPen + +but there are ``pointPen`` variants of several other pens, included +alongside the modules for their segment-oriented version. + New pens can be written by sub-classing the :class:`.AbstractPen` or, -somewhat more practically, :class:`.BasePen` classes. The Pen Protocol is -documented on the :ref:`basePen` page. +somewhat more practically, :class:`.BasePen` classes. The general Pen +Protocol is documented on the :ref:`basePen` page: + +.. toctree:: + :maxdepth: 1 + + basePen + +Some of the platform-specific pen modules rely on importing external +Python libraries; these cases are noted on the relevant pens' pages. + + + +Pen modules +----------- + -.. rubric:: Pen modules: - :heading-level: 3 .. toctree:: :maxdepth: 1 areaPen - basePen boundsPen - cocoaPen cu2quPen filterPen - freetypePen momentsPen perimeterPen pointInsidePen - pointPen - qtPen recordingPen - reportLabPen reverseContourPen roundingPen statisticsPen @@ -47,5 +80,15 @@ documented on the :ref:`basePen` page. teePen transformPen ttGlyphPen + +.. toctree:: + :maxdepth: 1 + + cairoPen + cocoaPen + freetypePen + qtPen + quartzPen + reportLabPen wxPen From 5d131dd0f07cbbb3724f49932792662a9cea935a Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:23:02 +0000 Subject: [PATCH 009/105] Docs fixes for varLib. --- Doc/source/varLib/builder.rst | 1 - Doc/source/varLib/cff.rst | 1 - Doc/source/varLib/errors.rst | 1 - Doc/source/varLib/featureVars.rst | 1 - Doc/source/varLib/index.rst | 25 ++++++++++++++++++------ Doc/source/varLib/instancer.rst | 1 - Doc/source/varLib/interpolatable.rst | 4 +++- Doc/source/varLib/interpolate_layout.rst | 1 - Doc/source/varLib/iup.rst | 1 - Doc/source/varLib/merger.rst | 1 - Doc/source/varLib/models.rst | 1 - Doc/source/varLib/mutator.rst | 1 - Doc/source/varLib/mvar.rst | 3 +-- Doc/source/varLib/plot.rst | 4 +++- Doc/source/varLib/varStore.rst | 1 - 15 files changed, 26 insertions(+), 21 deletions(-) diff --git a/Doc/source/varLib/builder.rst b/Doc/source/varLib/builder.rst index 3da3d32c14..909e09a87a 100644 --- a/Doc/source/varLib/builder.rst +++ b/Doc/source/varLib/builder.rst @@ -3,6 +3,5 @@ builder ####### .. automodule:: fontTools.varLib.builder - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/cff.rst b/Doc/source/varLib/cff.rst index 62e11c737f..95f34bf076 100644 --- a/Doc/source/varLib/cff.rst +++ b/Doc/source/varLib/cff.rst @@ -3,6 +3,5 @@ cff ### .. automodule:: fontTools.varLib.cff - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/errors.rst b/Doc/source/varLib/errors.rst index b761854c71..1b449e8d21 100644 --- a/Doc/source/varLib/errors.rst +++ b/Doc/source/varLib/errors.rst @@ -3,6 +3,5 @@ errors ###### .. automodule:: fontTools.varLib.errors - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/featureVars.rst b/Doc/source/varLib/featureVars.rst index da73560f81..b2805fac5f 100644 --- a/Doc/source/varLib/featureVars.rst +++ b/Doc/source/varLib/featureVars.rst @@ -3,6 +3,5 @@ featureVars ########### .. automodule:: fontTools.varLib.featureVars - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/index.rst b/Doc/source/varLib/index.rst index 32f428eb55..f8f5e2eef6 100644 --- a/Doc/source/varLib/index.rst +++ b/Doc/source/varLib/index.rst @@ -2,8 +2,23 @@ varLib: Support for OpenType Variations ####################################### +.. contents:: On this page: + :local: + +.. rubric:: Overview + :heading-level: 2 + +The :py:mod:`fontTools.varLib` package contains a number of classes and routines +for handling, building and interpolating variable font data. These routines +rely on a common set of concepts, many of which are equivalent to concepts +in the OpenType Specification, but some of which are unique to :py:mod:`varLib`. + + +Supporting modules +------------------ + .. toctree:: - :maxdepth: 2 + :maxdepth: 1 builder cff @@ -20,10 +35,6 @@ varLib: Support for OpenType Variations plot varStore -The ``fontTools.varLib`` package contains a number of classes and routines -for handling, building and interpolating variable font data. These routines -rely on a common set of concepts, many of which are equivalent to concepts -in the OpenType Specification, but some of which are unique to ``varLib``. Terminology ----------- @@ -108,7 +119,9 @@ support scalar 0 below its minimum or above its maximum. +Package contents +---------------- + .. automodule:: fontTools.varLib - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/instancer.rst b/Doc/source/varLib/instancer.rst index 8776de313c..91f416e681 100644 --- a/Doc/source/varLib/instancer.rst +++ b/Doc/source/varLib/instancer.rst @@ -3,6 +3,5 @@ instancer ######### .. automodule:: fontTools.varLib.instancer - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/interpolatable.rst b/Doc/source/varLib/interpolatable.rst index 1120a982a6..cf8f1b871d 100644 --- a/Doc/source/varLib/interpolatable.rst +++ b/Doc/source/varLib/interpolatable.rst @@ -2,7 +2,9 @@ interpolatable ############## +Note also that :mod:`varLib.interpolatable` supports some :doc:`optional ` +external libraries. + .. automodule:: fontTools.varLib.interpolatable - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/interpolate_layout.rst b/Doc/source/varLib/interpolate_layout.rst index a9655b53b4..752f748b02 100644 --- a/Doc/source/varLib/interpolate_layout.rst +++ b/Doc/source/varLib/interpolate_layout.rst @@ -3,6 +3,5 @@ interpolate_layout ################## .. automodule:: fontTools.varLib.interpolate_layout - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/iup.rst b/Doc/source/varLib/iup.rst index b096788b41..336b231c16 100644 --- a/Doc/source/varLib/iup.rst +++ b/Doc/source/varLib/iup.rst @@ -3,6 +3,5 @@ iup ### .. automodule:: fontTools.varLib.iup - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/merger.rst b/Doc/source/varLib/merger.rst index cf0a5a1c9c..37383aa65e 100644 --- a/Doc/source/varLib/merger.rst +++ b/Doc/source/varLib/merger.rst @@ -3,6 +3,5 @@ merger ###### .. automodule:: fontTools.varLib.merger - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/models.rst b/Doc/source/varLib/models.rst index f59f0b84c3..e6c7fa8acd 100644 --- a/Doc/source/varLib/models.rst +++ b/Doc/source/varLib/models.rst @@ -3,6 +3,5 @@ models ###### .. automodule:: fontTools.varLib.models - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/mutator.rst b/Doc/source/varLib/mutator.rst index fffa80382b..e606ab8679 100644 --- a/Doc/source/varLib/mutator.rst +++ b/Doc/source/varLib/mutator.rst @@ -3,6 +3,5 @@ mutator ####### .. automodule:: fontTools.varLib.mutator - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/mvar.rst b/Doc/source/varLib/mvar.rst index 8c59a310da..ec490d90f6 100644 --- a/Doc/source/varLib/mvar.rst +++ b/Doc/source/varLib/mvar.rst @@ -3,8 +3,7 @@ mvar #### .. automodule:: fontTools.varLib.mvar - :inherited-members: :members: :undoc-members: -.. data:: fontTools.varLib.mvar.MVAR_ENTRIES \ No newline at end of file +.. data:: fontTools.varLib.mvar.MVAR_ENTRIES diff --git a/Doc/source/varLib/plot.rst b/Doc/source/varLib/plot.rst index a722a2d617..77322f793e 100644 --- a/Doc/source/varLib/plot.rst +++ b/Doc/source/varLib/plot.rst @@ -2,7 +2,9 @@ plot #### + Note also that :mod:`varLib.plot` supports some :doc:`optional ` + external libraries. + .. automodule:: fontTools.varLib.plot - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/varLib/varStore.rst b/Doc/source/varLib/varStore.rst index cc91101e86..390f32b8a4 100644 --- a/Doc/source/varLib/varStore.rst +++ b/Doc/source/varLib/varStore.rst @@ -3,6 +3,5 @@ varStore ######## .. automodule:: fontTools.varLib.varStore - :inherited-members: :members: :undoc-members: From 826891a02a6d56061ebcf919ea5c1e8b9bd805cf Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:24:50 +0000 Subject: [PATCH 010/105] Regularize docs pages in misc subsection. --- Doc/source/misc/arrayTools.rst | 3 +-- Doc/source/misc/bezierTools.rst | 1 - Doc/source/misc/classifyTools.rst | 7 +++---- Doc/source/misc/cliTools.rst | 1 - Doc/source/misc/configTools.rst | 9 +++++---- Doc/source/misc/eexec.rst | 7 +++---- Doc/source/misc/encodingTools.rst | 7 +++---- Doc/source/misc/etree.rst | 13 ++++++++----- Doc/source/misc/filenames.rst | 6 +++--- Doc/source/misc/fixedTools.rst | 1 - Doc/source/misc/index.rst | 10 +++++++--- Doc/source/misc/intTools.rst | 1 + Doc/source/misc/loggingTools.rst | 2 +- Doc/source/misc/macCreatorType.rst | 8 ++++++-- Doc/source/misc/macRes.rst | 17 ++++++++++++++--- Doc/source/misc/plistlib.rst | 1 + Doc/source/misc/psCharStrings.rst | 7 +++---- Doc/source/misc/psLib.rst | 7 +++---- Doc/source/misc/psOperators.rst | 7 +++---- Doc/source/misc/sstruct.rst | 7 +++---- Doc/source/misc/symfont.rst | 10 ++++++---- Doc/source/misc/testTools.rst | 7 +++---- Doc/source/misc/textTools.rst | 7 +++---- Doc/source/misc/timeTools.rst | 7 +++---- Doc/source/misc/transform.rst | 7 +++---- Doc/source/misc/xmlReader.rst | 7 +++---- Doc/source/misc/xmlWriter.rst | 7 +++---- 27 files changed, 92 insertions(+), 82 deletions(-) diff --git a/Doc/source/misc/arrayTools.rst b/Doc/source/misc/arrayTools.rst index d996cc2030..74c13bf2e2 100644 --- a/Doc/source/misc/arrayTools.rst +++ b/Doc/source/misc/arrayTools.rst @@ -3,7 +3,6 @@ arrayTools: Various array and rectangle tools ############################################# .. 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..5efdd353c2 100644 --- a/Doc/source/misc/bezierTools.rst +++ b/Doc/source/misc/bezierTools.rst @@ -3,6 +3,5 @@ bezierTools: Routines for working with Bezier curves #################################################### .. 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..9bc4e657cb 100644 --- a/Doc/source/misc/classifyTools.rst +++ b/Doc/source/misc/classifyTools.rst @@ -1,8 +1,7 @@ -############# -classifyTools -############# +########################################### +classifyTools: Tools for set classification +########################################### .. 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..ea0569ee60 100644 --- a/Doc/source/misc/cliTools.rst +++ b/Doc/source/misc/cliTools.rst @@ -3,6 +3,5 @@ cliTools: Utilities for command-line interfaces and console scripts ################################################################### .. 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..b6d98a7fcd 100644 --- a/Doc/source/misc/eexec.rst +++ b/Doc/source/misc/eexec.rst @@ -1,8 +1,7 @@ -############################################################### -eexec: PostScript charstring encryption and decryption routines -############################################################### +################################################################### +eexec: Routines for PostScript CharString encryption and decryption +################################################################### .. 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..89771836a7 100644 --- a/Doc/source/misc/encodingTools.rst +++ b/Doc/source/misc/encodingTools.rst @@ -1,8 +1,7 @@ -############# -encodingTools -############# +########################################### +encodingTools: Tools for OpenType encodings +########################################### .. 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..7f8699c9f5 100644 --- a/Doc/source/misc/etree.rst +++ b/Doc/source/misc/etree.rst @@ -1,8 +1,11 @@ -##### -etree -##### +############################################## +etree: Tools for accessing the ElementTree API +############################################## + +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..fc6dd73039 100644 --- a/Doc/source/misc/filenames.rst +++ b/Doc/source/misc/filenames.rst @@ -1,6 +1,6 @@ -########################################################## -filenames: Implements UFO User Name to File Name Algorithm -########################################################## +################################################################### +filenames: Implementation of UFO's User-Name-to-File-Name algorithm +################################################################### .. automodule:: fontTools.misc.filenames :members: userNameToFileName diff --git a/Doc/source/misc/fixedTools.rst b/Doc/source/misc/fixedTools.rst index d3785f43f1..b6187d8d1c 100644 --- a/Doc/source/misc/fixedTools.rst +++ b/Doc/source/misc/fixedTools.rst @@ -3,6 +3,5 @@ fixedTools: Tools for working with fixed-point numbers ###################################################### .. 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..8f9b86f8b2 100644 --- a/Doc/source/misc/index.rst +++ b/Doc/source/misc/index.rst @@ -2,11 +2,15 @@ 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. +.. 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..648aa1562a 100644 --- a/Doc/source/misc/intTools.rst +++ b/Doc/source/misc/intTools.rst @@ -4,3 +4,4 @@ intTools: Tools for working with integer values .. automodule:: fontTools.misc.intTools :members: + :undoc-members: diff --git a/Doc/source/misc/loggingTools.rst b/Doc/source/misc/loggingTools.rst index 157e02095e..8fa0087e14 100644 --- a/Doc/source/misc/loggingTools.rst +++ b/Doc/source/misc/loggingTools.rst @@ -1,5 +1,5 @@ ################################################################### -loggingTools: tools for interfacing with the Python logging package +loggingTools: Tools for interfacing with the Python logging package ################################################################### .. automodule:: fontTools.misc.loggingTools diff --git a/Doc/source/misc/macCreatorType.rst b/Doc/source/misc/macCreatorType.rst index 809098dc7d..1198de3d00 100644 --- a/Doc/source/misc/macCreatorType.rst +++ b/Doc/source/misc/macCreatorType.rst @@ -2,8 +2,12 @@ macCreatorType: Functions for working with Mac file attributes ############################################################## -This module requires the `xattr `_ module -to be installed in order to function correctly. +.. 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..280b98fad1 100644 --- a/Doc/source/misc/macRes.rst +++ b/Doc/source/misc/macRes.rst @@ -2,10 +2,21 @@ 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. +.. rubric:: Overview: + :heading-level: 2 -The Inside Macintosh volume `More Macintosh Toolbox `_ explains the structure of resource and data 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. + +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..6d96c0d72b 100644 --- a/Doc/source/misc/plistlib.rst +++ b/Doc/source/misc/plistlib.rst @@ -4,3 +4,4 @@ plistlib: Tools for handling .plist files .. 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..18539184f8 100644 --- a/Doc/source/misc/psCharStrings.rst +++ b/Doc/source/misc/psCharStrings.rst @@ -1,8 +1,7 @@ -############# -psCharStrings -############# +##################################################### +psCharStrings: Tools for working with CharString data +##################################################### .. 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..8853fce824 100644 --- a/Doc/source/misc/psLib.rst +++ b/Doc/source/misc/psLib.rst @@ -1,8 +1,7 @@ -##### -psLib -##### +############################################# +psLib: Tools for working with PostScript data +############################################# .. 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..33014e027f 100644 --- a/Doc/source/misc/psOperators.rst +++ b/Doc/source/misc/psOperators.rst @@ -1,8 +1,7 @@ -########### -psOperators -########### +######################################################## +psOperators: Tools for working with PostScript operators +######################################################## .. 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..2cc6e59bfa 100644 --- a/Doc/source/misc/sstruct.rst +++ b/Doc/source/misc/sstruct.rst @@ -1,8 +1,7 @@ -####### -sstruct -####### +################################################## +sstruct: Tools for working with Python struct data +################################################## .. 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..f47ac31e03 100644 --- a/Doc/source/misc/symfont.rst +++ b/Doc/source/misc/symfont.rst @@ -1,8 +1,10 @@ -####### -symfont -####### +#################################################################### +symfont: Tools for working with Beziers through symbolic mathematics +#################################################################### + 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..26543d7033 100644 --- a/Doc/source/misc/testTools.rst +++ b/Doc/source/misc/testTools.rst @@ -1,8 +1,7 @@ -######### -testTools -######### +################################# +testTools: Tools for unit testing +################################# .. 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..7b58891eaa 100644 --- a/Doc/source/misc/textTools.rst +++ b/Doc/source/misc/textTools.rst @@ -1,8 +1,7 @@ -######### -textTools -######### +########################################### +textTools: Tools for working with text data +########################################### .. 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..57596c5e0f 100644 --- a/Doc/source/misc/timeTools.rst +++ b/Doc/source/misc/timeTools.rst @@ -1,8 +1,7 @@ -######### -timeTools -######### +##################################################### +timeTools: Tools for working with OpenType timestamps +##################################################### .. 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..71e1c4a868 100644 --- a/Doc/source/misc/transform.rst +++ b/Doc/source/misc/transform.rst @@ -1,8 +1,7 @@ -######### -transform -######### +######################################################### +transform: Tools for working with transformation matrices +######################################################### .. 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..178f5a5442 100644 --- a/Doc/source/misc/xmlReader.rst +++ b/Doc/source/misc/xmlReader.rst @@ -1,8 +1,7 @@ -######### -xmlReader -######### +##################################### +xmlReader: Tools for reading XML data +##################################### .. 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..aec40ce8ca 100644 --- a/Doc/source/misc/xmlWriter.rst +++ b/Doc/source/misc/xmlWriter.rst @@ -1,8 +1,7 @@ -######### -xmlWriter -######### +##################################### +xmlWriter: Tools for writing XML data +##################################### .. automodule:: fontTools.misc.xmlWriter - :inherited-members: :members: :undoc-members: From a5f380e645ab15650acf2c155cc9d541f3caf08f Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:26:05 +0000 Subject: [PATCH 011/105] Docs improvements and missing pieces for unicodedata package. --- Doc/source/unicodedata/Blocks.rst | 1 - Doc/source/unicodedata/OTTags.rst | 1 - Doc/source/unicodedata/ScriptExtensions.rst | 1 - Doc/source/unicodedata/Scripts.rst | 1 - Doc/source/unicodedata/index.rst | 43 ++++++++++++++++----- 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Doc/source/unicodedata/Blocks.rst b/Doc/source/unicodedata/Blocks.rst index 5d01da7ecd..97f8ac5edd 100644 --- a/Doc/source/unicodedata/Blocks.rst +++ b/Doc/source/unicodedata/Blocks.rst @@ -3,7 +3,6 @@ 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..f766b5ffb0 100644 --- a/Doc/source/unicodedata/OTTags.rst +++ b/Doc/source/unicodedata/OTTags.rst @@ -3,7 +3,6 @@ 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..5ffff5d2b9 100644 --- a/Doc/source/unicodedata/ScriptExtensions.rst +++ b/Doc/source/unicodedata/ScriptExtensions.rst @@ -3,7 +3,6 @@ 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..b056cea65a 100644 --- a/Doc/source/unicodedata/Scripts.rst +++ b/Doc/source/unicodedata/Scripts.rst @@ -3,7 +3,6 @@ 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..ad276f112b 100644 --- a/Doc/source/unicodedata/index.rst +++ b/Doc/source/unicodedata/index.rst @@ -1,16 +1,39 @@ -########### -unicodedata -########### +########################################################################### +unicodedata: Interface to character and script data in Unicode and OpenType +########################################################################### -.. toctree:: - :maxdepth: 1 +.. contents:: On this page: + :local: + +.. rubric:: Overview: + :heading-level: 2 - Blocks - OTTags - ScriptExtensions - Scripts +: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 From 21e611b858a6898adbea7243c03979e0bcc6fc77 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:27:47 +0000 Subject: [PATCH 012/105] Docs, improve subset pages --- Doc/source/subset/cff.rst | 1 - Doc/source/subset/index.rst | 1 - 2 files changed, 2 deletions(-) diff --git a/Doc/source/subset/cff.rst b/Doc/source/subset/cff.rst index 8c21c3966e..dcdd227c93 100644 --- a/Doc/source/subset/cff.rst +++ b/Doc/source/subset/cff.rst @@ -3,6 +3,5 @@ cff ### .. automodule:: fontTools.subset.cff - :inherited-members: :members: :undoc-members: diff --git a/Doc/source/subset/index.rst b/Doc/source/subset/index.rst index 1bc255b0f3..9713fdf359 100644 --- a/Doc/source/subset/index.rst +++ b/Doc/source/subset/index.rst @@ -8,6 +8,5 @@ subset: Generate subsets of fonts or optimize file sizes cff .. automodule:: fontTools.subset - :inherited-members: :members: :undoc-members: From 932a3d8e5f29c8a9c35905eb92c95955d7b1a700 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:29:42 +0000 Subject: [PATCH 013/105] Docs, feaLib additions. --- Doc/source/feaLib/ast.rst | 35 ++++++++++++++++++++++ Doc/source/feaLib/index.rst | 57 +++++++++++++++++++++++++----------- Doc/source/feaLib/parser.rst | 52 ++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 17 deletions(-) create mode 100644 Doc/source/feaLib/ast.rst create mode 100644 Doc/source/feaLib/parser.rst diff --git a/Doc/source/feaLib/ast.rst b/Doc/source/feaLib/ast.rst new file mode 100644 index 0000000000..4213dd4864 --- /dev/null +++ b/Doc/source/feaLib/ast.rst @@ -0,0 +1,35 @@ +#################################################### +ast: Interrogate and generate OpenType feature files +#################################################### + +.. rubric:: Overview: + :heading-level: 2 + +feaLib's :mod:`.ast` module provides classes that represent the objects +and structures used to define OpenType feature lookups in the ``fea`` +syntax. After your code has parsed a ``.fea`` file into an abstract +syntax tree (AST), you can use these classes to modify existing +lookups or create new lookups and features. + +The root of the AST representation of a parsed ``.fea`` file is a +:class:`fontTools.feaLib.ast.FeatureFile` object. You can walk the +tree by examining its :attr:`statements` attribute. Nodes in the +tree have an :meth:`asFea` method that will return a ``.fea`` +formated string representation, including correct indentation of block elements. + + +.. _`glyph-containing object`: +.. _`glyph-containing objects`: + +In the below, a **glyph-containing object** is an object of one of the following +classes: :class:`GlyphName`, :class:`GlyphClass`, :class:`GlyphClassName`. + +.. automodule:: fontTools.feaLib.ast + :members: + :undoc-members: + + .. rubric:: Module members + :heading-level: 2 + + + diff --git a/Doc/source/feaLib/index.rst b/Doc/source/feaLib/index.rst index c25532caa3..46b82240d7 100644 --- a/Doc/source/feaLib/index.rst +++ b/Doc/source/feaLib/index.rst @@ -2,10 +2,34 @@ feaLib: Read and write OpenType feature files ############################################# -fontTools' ``feaLib`` allows for the creation and parsing of Adobe +.. 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 +37,27 @@ 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 +------------------------ .. automodule:: fontTools.feaLib.builder - :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString + :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString, Builder + :undoc-members: -Generation/Interrogation ------------------------- -.. _`glyph-containing object`: -.. _`glyph-containing objects`: +fontTools.feaLib.lookupDebugInfo +-------------------------------- + +.. automodule:: fontTools.feaLib.lookupDebugInfo + :members: + :undoc-members: + -In the below, a **glyph-containing object** is an object of one of the following -classes: :class:`GlyphName`, :class:`GlyphClass`, :class:`GlyphClassName`. +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..bcae5afb8a --- /dev/null +++ b/Doc/source/feaLib/parser.rst @@ -0,0 +1,52 @@ +################################################# +parser: Lexing and parsing OpenType feature files +################################################# + +.. 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 +------- + +.. automodule:: fontTools.feaLib.parser + :members: + :undoc-members: + + +Lexing +------ + +.. automodule:: fontTools.feaLib.lexer + :members: + :undoc-members: + + + +fontTools.feaLib.variableScalar +------------------------------- + +.. automodule:: fontTools.feaLib.variableScalar + :members: + :undoc-members: + + + +fontTools.feaLib.location +------------------------- + +.. automodule:: fontTools.feaLib.location + :members: + :undoc-members: + From bbe72ca995df29e55e8873fdb54e87df192973a3 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:30:00 +0000 Subject: [PATCH 014/105] Docs, otlLib minor. --- Doc/source/otlLib/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/source/otlLib/index.rst b/Doc/source/otlLib/index.rst index b1502b0537..85ee6e6651 100644 --- a/Doc/source/otlLib/index.rst +++ b/Doc/source/otlLib/index.rst @@ -2,6 +2,10 @@ otlLib: Routines for working with OpenType Layout ################################################# +.. contents:: On this page: + :local: + + The ``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, From 96283fe8ea1ecf0c393d63d7e35bbcb2f9facd17 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:30:52 +0000 Subject: [PATCH 015/105] Docs, additions in encoding package. --- Doc/source/encodings/index.rst | 46 ++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/Doc/source/encodings/index.rst b/Doc/source/encodings/index.rst index f35e3806eb..e78d0ea11a 100644 --- a/Doc/source/encodings/index.rst +++ b/Doc/source/encodings/index.rst @@ -2,20 +2,56 @@ encodings: Support for OpenType-specific character 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 +-------------------------- + +.. automodule:: fontTools.encodings.codecs + :members: + :undoc-members: + + +fontTools.encodings.MacRoman +---------------------------- + +.. automodule:: fontTools.encodings.MacRoman + :members: + :undoc-members: + +fontTools.encodings.StandardEncoding +------------------------------------ + +.. automodule:: fontTools.encodings.StandardEncoding + :members: + :undoc-members: + From 505f5a5a0b347448997db7653232637d838bf93c Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 17:31:27 +0000 Subject: [PATCH 016/105] Docs, overview and additions in voltLib. --- Doc/source/voltLib/index.rst | 50 +++++++++++++++++++++++++++----- Doc/source/voltLib/voltToFea.rst | 1 - 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/Doc/source/voltLib/index.rst b/Doc/source/voltLib/index.rst index a0ced85de0..2ab86726fe 100644 --- a/Doc/source/voltLib/index.rst +++ b/Doc/source/voltLib/index.rst @@ -2,33 +2,67 @@ voltLib: Read and write MS VOLT projects ######################################## +.. 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 +--------------------- .. automodule:: fontTools.voltLib.ast + :members: :undoc-members: -error ------ + +fontTools.voltLib.error +----------------------- .. automodule:: fontTools.voltLib.error + :members: :undoc-members: -lexer ------ + +fontTools.voltLib.lexer +----------------------- .. automodule:: fontTools.voltLib.lexer + :members: :undoc-members: -parser ------- + +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..43c0db70cb 100644 --- a/Doc/source/voltLib/voltToFea.rst +++ b/Doc/source/voltLib/voltToFea.rst @@ -3,6 +3,5 @@ voltToFea: Convert MS VOLT to AFDKO feature files ################################################# .. automodule:: fontTools.voltLib.voltToFea - :inherited-members: :members: :undoc-members: From 3ad72a1810a7e92d57f144f51c1732ac67e9daa6 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 21:10:47 +0000 Subject: [PATCH 017/105] Apply currentmodule config. Suppresses autodoc errors and warnings. --- Doc/source/encodings/index.rst | 8 ++++++++ Doc/source/feaLib/ast.rst | 2 ++ Doc/source/feaLib/index.rst | 8 ++++++++ Doc/source/feaLib/parser.rst | 10 ++++++++++ Doc/source/misc/arrayTools.rst | 2 ++ Doc/source/misc/bezierTools.rst | 2 ++ Doc/source/misc/classifyTools.rst | 2 ++ Doc/source/misc/cliTools.rst | 2 ++ Doc/source/misc/eexec.rst | 2 ++ Doc/source/misc/encodingTools.rst | 2 ++ Doc/source/misc/etree.rst | 2 ++ Doc/source/misc/filenames.rst | 2 ++ Doc/source/misc/fixedTools.rst | 2 ++ Doc/source/misc/index.rst | 2 ++ Doc/source/misc/intTools.rst | 2 ++ Doc/source/misc/loggingTools.rst | 2 ++ Doc/source/misc/macCreatorType.rst | 2 ++ Doc/source/misc/macRes.rst | 2 ++ Doc/source/misc/plistlib.rst | 2 ++ Doc/source/misc/psCharStrings.rst | 2 ++ Doc/source/misc/psLib.rst | 2 ++ Doc/source/misc/psOperators.rst | 2 ++ Doc/source/misc/sstruct.rst | 2 ++ Doc/source/misc/symfont.rst | 6 ++++-- Doc/source/misc/testTools.rst | 2 ++ Doc/source/misc/textTools.rst | 2 ++ Doc/source/misc/timeTools.rst | 2 ++ Doc/source/misc/transform.rst | 2 ++ Doc/source/misc/xmlReader.rst | 2 ++ Doc/source/misc/xmlWriter.rst | 2 ++ Doc/source/otlLib/index.rst | 4 +++- Doc/source/subset/cff.rst | 8 +++++--- Doc/source/subset/index.rst | 2 ++ Doc/source/unicodedata/Blocks.rst | 2 ++ Doc/source/unicodedata/OTTags.rst | 2 ++ Doc/source/unicodedata/ScriptExtensions.rst | 2 ++ Doc/source/unicodedata/Scripts.rst | 2 ++ Doc/source/unicodedata/index.rst | 2 ++ Doc/source/voltLib/index.rst | 10 ++++++++++ Doc/source/voltLib/voltToFea.rst | 2 ++ 40 files changed, 114 insertions(+), 6 deletions(-) diff --git a/Doc/source/encodings/index.rst b/Doc/source/encodings/index.rst index e78d0ea11a..97f73d38e9 100644 --- a/Doc/source/encodings/index.rst +++ b/Doc/source/encodings/index.rst @@ -2,6 +2,8 @@ encodings: Support for OpenType-specific character encodings ############################################################ +.. currentmodule:: fontTools.encodings + .. contents:: On this page: :local: @@ -36,6 +38,8 @@ Encoding. fontTools.encodings.codecs -------------------------- +.. currentmodule:: fontTools.encodings.codecs + .. automodule:: fontTools.encodings.codecs :members: :undoc-members: @@ -44,6 +48,8 @@ fontTools.encodings.codecs fontTools.encodings.MacRoman ---------------------------- +.. currentmodule:: fontTools.encodings.MacRoman + .. automodule:: fontTools.encodings.MacRoman :members: :undoc-members: @@ -51,6 +57,8 @@ fontTools.encodings.MacRoman 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 index 4213dd4864..b862d42698 100644 --- a/Doc/source/feaLib/ast.rst +++ b/Doc/source/feaLib/ast.rst @@ -2,6 +2,8 @@ ast: Interrogate and generate OpenType feature files #################################################### +.. currentmodule:: fontTools.feaLib.astt + .. rubric:: Overview: :heading-level: 2 diff --git a/Doc/source/feaLib/index.rst b/Doc/source/feaLib/index.rst index 46b82240d7..a0236ab431 100644 --- a/Doc/source/feaLib/index.rst +++ b/Doc/source/feaLib/index.rst @@ -2,6 +2,8 @@ feaLib: Read and write OpenType feature files ############################################# +.. currentmodule:: fontTools.feaLib + .. contents:: On this page: :local: @@ -42,6 +44,8 @@ using the classes in the :mod:`fontTools.feaLib.ast` module. fontTools.feaLib.builder ------------------------ +.. currentmodule:: fontTools.feaLib.builder + .. automodule:: fontTools.feaLib.builder :members: addOpenTypeFeatures, addOpenTypeFeaturesFromString, Builder :undoc-members: @@ -50,6 +54,8 @@ fontTools.feaLib.builder fontTools.feaLib.lookupDebugInfo -------------------------------- +.. currentmodule:: fontTools.feaLib.lookupDebugInfo + .. automodule:: fontTools.feaLib.lookupDebugInfo :members: :undoc-members: @@ -58,6 +64,8 @@ fontTools.feaLib.lookupDebugInfo fontTools.feaLib.error ---------------------- +.. currentmodule:: fontTools.feaLib.error + .. automodule:: fontTools.feaLib.error :members: :undoc-members: diff --git a/Doc/source/feaLib/parser.rst b/Doc/source/feaLib/parser.rst index bcae5afb8a..62c492c2f1 100644 --- a/Doc/source/feaLib/parser.rst +++ b/Doc/source/feaLib/parser.rst @@ -2,6 +2,8 @@ parser: Lexing and parsing OpenType feature files ################################################# +.. currentmodule:: fontTools.feaLib + .. contents:: On this page: :local: @@ -20,6 +22,8 @@ utility modules. Parsing ------- +.. currentmodule:: fontTools.feaLib.parser + .. automodule:: fontTools.feaLib.parser :members: :undoc-members: @@ -28,6 +32,8 @@ Parsing Lexing ------ +.. currentmodule:: fontTools.feaLib.lexer + .. automodule:: fontTools.feaLib.lexer :members: :undoc-members: @@ -37,6 +43,8 @@ Lexing fontTools.feaLib.variableScalar ------------------------------- +.. currentmodule:: fontTools.feaLib.variableScalar + .. automodule:: fontTools.feaLib.variableScalar :members: :undoc-members: @@ -46,6 +54,8 @@ fontTools.feaLib.variableScalar fontTools.feaLib.location ------------------------- +.. currentmodule:: fontTools.feaLib.location + .. automodule:: fontTools.feaLib.location :members: :undoc-members: diff --git a/Doc/source/misc/arrayTools.rst b/Doc/source/misc/arrayTools.rst index 74c13bf2e2..00ed11772d 100644 --- a/Doc/source/misc/arrayTools.rst +++ b/Doc/source/misc/arrayTools.rst @@ -2,6 +2,8 @@ arrayTools: Various array and rectangle tools ############################################# +.. currentmodule:: fontTools.misc.arrayTools + .. automodule:: fontTools.misc.arrayTools :members: :undoc-members: diff --git a/Doc/source/misc/bezierTools.rst b/Doc/source/misc/bezierTools.rst index 5efdd353c2..5103555f7c 100644 --- a/Doc/source/misc/bezierTools.rst +++ b/Doc/source/misc/bezierTools.rst @@ -2,6 +2,8 @@ bezierTools: Routines for working with Bezier curves #################################################### +.. currentmodule:: fontTools.misc.bezierTools + .. automodule:: fontTools.misc.bezierTools :members: :undoc-members: diff --git a/Doc/source/misc/classifyTools.rst b/Doc/source/misc/classifyTools.rst index 9bc4e657cb..d18127d159 100644 --- a/Doc/source/misc/classifyTools.rst +++ b/Doc/source/misc/classifyTools.rst @@ -2,6 +2,8 @@ classifyTools: Tools for set classification ########################################### +.. currentmodule:: fontTools.misc.classifyTools + .. automodule:: fontTools.misc.classifyTools :members: :undoc-members: diff --git a/Doc/source/misc/cliTools.rst b/Doc/source/misc/cliTools.rst index ea0569ee60..01a50cc721 100644 --- a/Doc/source/misc/cliTools.rst +++ b/Doc/source/misc/cliTools.rst @@ -2,6 +2,8 @@ cliTools: Utilities for command-line interfaces and console scripts ################################################################### +.. currentmodule:: fontTools.misc.cliTools + .. automodule:: fontTools.misc.cliTools :members: :undoc-members: diff --git a/Doc/source/misc/eexec.rst b/Doc/source/misc/eexec.rst index b6d98a7fcd..b0b5537183 100644 --- a/Doc/source/misc/eexec.rst +++ b/Doc/source/misc/eexec.rst @@ -2,6 +2,8 @@ eexec: Routines for PostScript CharString encryption and decryption ################################################################### +.. currentmodule:: fontTools.misc.eexec + .. automodule:: fontTools.misc.eexec :members: :undoc-members: diff --git a/Doc/source/misc/encodingTools.rst b/Doc/source/misc/encodingTools.rst index 89771836a7..1baceae710 100644 --- a/Doc/source/misc/encodingTools.rst +++ b/Doc/source/misc/encodingTools.rst @@ -2,6 +2,8 @@ encodingTools: Tools for OpenType encodings ########################################### +.. currentmodule:: fontTools.misc.encodingTools + .. automodule:: fontTools.misc.encodingTools :members: :undoc-members: diff --git a/Doc/source/misc/etree.rst b/Doc/source/misc/etree.rst index 7f8699c9f5..5f67b98540 100644 --- a/Doc/source/misc/etree.rst +++ b/Doc/source/misc/etree.rst @@ -2,6 +2,8 @@ etree: Tools for accessing the ElementTree API ############################################## +.. currentmodule:: fontTools.misc.etree + Note also that :mod:`etree` supports some :doc:`optional ` external libraries. diff --git a/Doc/source/misc/filenames.rst b/Doc/source/misc/filenames.rst index fc6dd73039..4ecd406049 100644 --- a/Doc/source/misc/filenames.rst +++ b/Doc/source/misc/filenames.rst @@ -2,6 +2,8 @@ filenames: Implementation of UFO's User-Name-to-File-Name algorithm ################################################################### +.. currentmodule:: fontTools.misc.filenames + .. automodule:: fontTools.misc.filenames :members: userNameToFileName :undoc-members: diff --git a/Doc/source/misc/fixedTools.rst b/Doc/source/misc/fixedTools.rst index b6187d8d1c..f6ea26167c 100644 --- a/Doc/source/misc/fixedTools.rst +++ b/Doc/source/misc/fixedTools.rst @@ -2,6 +2,8 @@ fixedTools: Tools for working with fixed-point numbers ###################################################### +.. currentmodule:: fontTools.misc.fixedTools + .. automodule:: fontTools.misc.fixedTools :members: :undoc-members: diff --git a/Doc/source/misc/index.rst b/Doc/source/misc/index.rst index 8f9b86f8b2..2ce315204b 100644 --- a/Doc/source/misc/index.rst +++ b/Doc/source/misc/index.rst @@ -2,6 +2,8 @@ misc: Miscellaneous libraries helpful for font engineering ########################################################## +.. currentmodule:: fontTools.misc + .. rubric:: Overview: :heading-level: 2 diff --git a/Doc/source/misc/intTools.rst b/Doc/source/misc/intTools.rst index 648aa1562a..33cc461aa5 100644 --- a/Doc/source/misc/intTools.rst +++ b/Doc/source/misc/intTools.rst @@ -2,6 +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 8fa0087e14..03cc398b41 100644 --- a/Doc/source/misc/loggingTools.rst +++ b/Doc/source/misc/loggingTools.rst @@ -2,6 +2,8 @@ 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 1198de3d00..93088208c0 100644 --- a/Doc/source/misc/macCreatorType.rst +++ b/Doc/source/misc/macCreatorType.rst @@ -2,6 +2,8 @@ macCreatorType: Functions for working with Mac file attributes ############################################################## +.. currentmodule:: fontTools.misc.macCreatorType + .. rubric:: Overview: :heading-level: 2 diff --git a/Doc/source/misc/macRes.rst b/Doc/source/misc/macRes.rst index 280b98fad1..c9e5c36acb 100644 --- a/Doc/source/misc/macRes.rst +++ b/Doc/source/misc/macRes.rst @@ -2,6 +2,8 @@ macRes: Tools for reading Mac resource forks ############################################ +.. currentmodule:: fontTools.misc.macRes + .. rubric:: Overview: :heading-level: 2 diff --git a/Doc/source/misc/plistlib.rst b/Doc/source/misc/plistlib.rst index 6d96c0d72b..7f78e03505 100644 --- a/Doc/source/misc/plistlib.rst +++ b/Doc/source/misc/plistlib.rst @@ -2,6 +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 18539184f8..34a9a8ccb1 100644 --- a/Doc/source/misc/psCharStrings.rst +++ b/Doc/source/misc/psCharStrings.rst @@ -2,6 +2,8 @@ psCharStrings: Tools for working with CharString data ##################################################### +.. currentmodule:: fontTools.misc.psCharStrings + .. automodule:: fontTools.misc.psCharStrings :members: :undoc-members: diff --git a/Doc/source/misc/psLib.rst b/Doc/source/misc/psLib.rst index 8853fce824..b0a100fc20 100644 --- a/Doc/source/misc/psLib.rst +++ b/Doc/source/misc/psLib.rst @@ -2,6 +2,8 @@ psLib: Tools for working with PostScript data ############################################# +.. currentmodule:: fontTools.misc.psLib + .. automodule:: fontTools.misc.psLib :members: :undoc-members: diff --git a/Doc/source/misc/psOperators.rst b/Doc/source/misc/psOperators.rst index 33014e027f..966e1ae8ef 100644 --- a/Doc/source/misc/psOperators.rst +++ b/Doc/source/misc/psOperators.rst @@ -2,6 +2,8 @@ psOperators: Tools for working with PostScript operators ######################################################## +.. currentmodule:: fontTools.misc.psOperators + .. automodule:: fontTools.misc.psOperators :members: :undoc-members: diff --git a/Doc/source/misc/sstruct.rst b/Doc/source/misc/sstruct.rst index 2cc6e59bfa..9e14907a06 100644 --- a/Doc/source/misc/sstruct.rst +++ b/Doc/source/misc/sstruct.rst @@ -2,6 +2,8 @@ sstruct: Tools for working with Python struct data ################################################## +.. currentmodule:: fontTools.misc.sstruct + .. automodule:: fontTools.misc.sstruct :members: :undoc-members: diff --git a/Doc/source/misc/symfont.rst b/Doc/source/misc/symfont.rst index f47ac31e03..2eb8159338 100644 --- a/Doc/source/misc/symfont.rst +++ b/Doc/source/misc/symfont.rst @@ -2,8 +2,10 @@ symfont: Tools for working with Beziers through symbolic mathematics #################################################################### - Note also that :mod:`misc.symfont` supports some :doc:`optional ` - external libraries. +.. currentmodule:: fontTools.misc.symfont + +Note also that :mod:`misc.symfont` supports some :doc:`optional ` +external libraries. .. automodule:: fontTools.misc.symfont :members: diff --git a/Doc/source/misc/testTools.rst b/Doc/source/misc/testTools.rst index 26543d7033..8baad6367c 100644 --- a/Doc/source/misc/testTools.rst +++ b/Doc/source/misc/testTools.rst @@ -2,6 +2,8 @@ testTools: Tools for unit testing ################################# +.. currentmodule:: fontTools.misc.testTools + .. automodule:: fontTools.misc.testTools :members: :undoc-members: diff --git a/Doc/source/misc/textTools.rst b/Doc/source/misc/textTools.rst index 7b58891eaa..50b11d1528 100644 --- a/Doc/source/misc/textTools.rst +++ b/Doc/source/misc/textTools.rst @@ -2,6 +2,8 @@ textTools: Tools for working with text data ########################################### +.. currentmodule:: fontTools.misc.textTools + .. automodule:: fontTools.misc.textTools :members: :undoc-members: diff --git a/Doc/source/misc/timeTools.rst b/Doc/source/misc/timeTools.rst index 57596c5e0f..256a2e5928 100644 --- a/Doc/source/misc/timeTools.rst +++ b/Doc/source/misc/timeTools.rst @@ -2,6 +2,8 @@ timeTools: Tools for working with OpenType timestamps ##################################################### +.. currentmodule:: fontTools.misc.timeTools + .. automodule:: fontTools.misc.timeTools :members: :undoc-members: diff --git a/Doc/source/misc/transform.rst b/Doc/source/misc/transform.rst index 71e1c4a868..7f8cb47636 100644 --- a/Doc/source/misc/transform.rst +++ b/Doc/source/misc/transform.rst @@ -2,6 +2,8 @@ transform: Tools for working with transformation matrices ######################################################### +.. currentmodule:: fontTools.misc.transform + .. automodule:: fontTools.misc.transform :members: :undoc-members: diff --git a/Doc/source/misc/xmlReader.rst b/Doc/source/misc/xmlReader.rst index 178f5a5442..fb276165e1 100644 --- a/Doc/source/misc/xmlReader.rst +++ b/Doc/source/misc/xmlReader.rst @@ -2,6 +2,8 @@ xmlReader: Tools for reading XML data ##################################### +.. currentmodule:: fontTools.misc.xmlReader + .. automodule:: fontTools.misc.xmlReader :members: :undoc-members: diff --git a/Doc/source/misc/xmlWriter.rst b/Doc/source/misc/xmlWriter.rst index aec40ce8ca..22550ee589 100644 --- a/Doc/source/misc/xmlWriter.rst +++ b/Doc/source/misc/xmlWriter.rst @@ -2,6 +2,8 @@ xmlWriter: Tools for writing XML data ##################################### +.. currentmodule:: fontTools.misc.xmlWriter + .. automodule:: fontTools.misc.xmlWriter :members: :undoc-members: diff --git a/Doc/source/otlLib/index.rst b/Doc/source/otlLib/index.rst index 85ee6e6651..bc6efa9053 100644 --- a/Doc/source/otlLib/index.rst +++ b/Doc/source/otlLib/index.rst @@ -2,11 +2,13 @@ otlLib: Routines for working with OpenType Layout ################################################# +.. currentmodule:: fontTools.otlLib + .. contents:: On this page: :local: -The ``fontTools.otlLib`` library provides routines to help you create the +The :mod:`fontTools.otlLib` library provides routines to help you create the subtables and other data structures you need when you are editing a font's ``GSUB`` and ``GPOS`` tables: substitution and positioning rules, anchors, lookups, coverage tables and so on. diff --git a/Doc/source/subset/cff.rst b/Doc/source/subset/cff.rst index dcdd227c93..bf9461dc86 100644 --- a/Doc/source/subset/cff.rst +++ b/Doc/source/subset/cff.rst @@ -1,6 +1,8 @@ -### -cff -### +############################################ +cff: Tools for subsetting CFF-flavored fonts +############################################ + +.. currentmodule:: fontTools.subset.cff .. automodule:: fontTools.subset.cff :members: diff --git a/Doc/source/subset/index.rst b/Doc/source/subset/index.rst index 9713fdf359..30067801d0 100644 --- a/Doc/source/subset/index.rst +++ b/Doc/source/subset/index.rst @@ -2,6 +2,8 @@ subset: Generate subsets of fonts or optimize file sizes ######################################################## +.. currentmodule:: fontTools.subset + .. toctree:: :maxdepth: 1 diff --git a/Doc/source/unicodedata/Blocks.rst b/Doc/source/unicodedata/Blocks.rst index 97f8ac5edd..7650f86725 100644 --- a/Doc/source/unicodedata/Blocks.rst +++ b/Doc/source/unicodedata/Blocks.rst @@ -2,6 +2,8 @@ Blocks ###### +.. currentmodule:: fontTools.unicodedata.Blocks + .. automodule:: fontTools.unicodedata.Blocks :members: :undoc-members: diff --git a/Doc/source/unicodedata/OTTags.rst b/Doc/source/unicodedata/OTTags.rst index f766b5ffb0..8e3571ab67 100644 --- a/Doc/source/unicodedata/OTTags.rst +++ b/Doc/source/unicodedata/OTTags.rst @@ -2,6 +2,8 @@ OTTags ###### +.. currentmodule:: fontTools.unicodedata.OTTags + .. automodule:: fontTools.unicodedata.OTTags :members: :undoc-members: diff --git a/Doc/source/unicodedata/ScriptExtensions.rst b/Doc/source/unicodedata/ScriptExtensions.rst index 5ffff5d2b9..47d0c92bec 100644 --- a/Doc/source/unicodedata/ScriptExtensions.rst +++ b/Doc/source/unicodedata/ScriptExtensions.rst @@ -2,6 +2,8 @@ ScriptExtensions ################ +.. currentmodule:: fontTools.unicodedata.ScriptExtensions + .. automodule:: fontTools.unicodedata.ScriptExtensions :members: :undoc-members: diff --git a/Doc/source/unicodedata/Scripts.rst b/Doc/source/unicodedata/Scripts.rst index b056cea65a..d8cc01e07b 100644 --- a/Doc/source/unicodedata/Scripts.rst +++ b/Doc/source/unicodedata/Scripts.rst @@ -2,6 +2,8 @@ Scripts ####### +.. currentmodule:: fontTools.unicodedata.Scripts + .. automodule:: fontTools.unicodedata.Scripts :members: :undoc-members: diff --git a/Doc/source/unicodedata/index.rst b/Doc/source/unicodedata/index.rst index ad276f112b..395dd43b49 100644 --- a/Doc/source/unicodedata/index.rst +++ b/Doc/source/unicodedata/index.rst @@ -2,6 +2,8 @@ unicodedata: Interface to character and script data in Unicode and OpenType ########################################################################### +.. currentmodule:: fontTools.unicodedata + .. contents:: On this page: :local: diff --git a/Doc/source/voltLib/index.rst b/Doc/source/voltLib/index.rst index 2ab86726fe..aa666c9682 100644 --- a/Doc/source/voltLib/index.rst +++ b/Doc/source/voltLib/index.rst @@ -2,6 +2,8 @@ voltLib: Read and write MS VOLT projects ######################################## +.. currentmodule:: fontTools.voltLib + .. contents:: On this page: :local: @@ -39,6 +41,8 @@ fontTools.voltLib fontTools.voltLib.ast --------------------- +.. currentmodule:: fontTools.voltLib.ast + .. automodule:: fontTools.voltLib.ast :members: :undoc-members: @@ -47,6 +51,8 @@ fontTools.voltLib.ast fontTools.voltLib.error ----------------------- +.. currentmodule:: fontTools.voltLib.error + .. automodule:: fontTools.voltLib.error :members: :undoc-members: @@ -55,6 +61,8 @@ fontTools.voltLib.error fontTools.voltLib.lexer ----------------------- +.. currentmodule:: fontTools.voltLib.lexer + .. automodule:: fontTools.voltLib.lexer :members: :undoc-members: @@ -63,6 +71,8 @@ fontTools.voltLib.lexer 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 43c0db70cb..393d3abaa7 100644 --- a/Doc/source/voltLib/voltToFea.rst +++ b/Doc/source/voltLib/voltToFea.rst @@ -2,6 +2,8 @@ voltToFea: Convert MS VOLT to AFDKO feature files ################################################# +.. currentmodule:: fontTools.voltLib.voltToFea + .. automodule:: fontTools.voltLib.voltToFea :members: :undoc-members: From 33b8f51f1c12a429b352ae20d3e9ff7ffa6b84b1 Mon Sep 17 00:00:00 2001 From: Nathan Williis Date: Mon, 9 Dec 2024 21:42:13 +0000 Subject: [PATCH 018/105] Clarify pen modules overview, add second-stage TOC and subtitles to landing page config. --- Doc/source/pens/basePen.rst | 6 ++-- Doc/source/pens/index.rst | 65 +++++++++++++++++++----------------- Doc/source/pens/pointPen.rst | 11 ++++-- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/Doc/source/pens/basePen.rst b/Doc/source/pens/basePen.rst index f5965b16c7..00e74df482 100644 --- a/Doc/source/pens/basePen.rst +++ b/Doc/source/pens/basePen.rst @@ -1,6 +1,6 @@ -####### -basePen -####### +############################################### +basePen: Base classes for segment-oriented pens +############################################### .. automodule:: fontTools.pens.basePen :members: diff --git a/Doc/source/pens/index.rst b/Doc/source/pens/index.rst index d85d5c0727..bad859de07 100644 --- a/Doc/source/pens/index.rst +++ b/Doc/source/pens/index.rst @@ -2,9 +2,6 @@ pens: Inspect and manipulate glyph outlines ########################################### -.. contents:: On this page: - :local: - .. rubric:: Overview: :heading-level: 2 @@ -17,42 +14,44 @@ such as a new glyph outline or a formatted image, but other pens analyze the outlines and return information about the glyph. Pens that alter or produce a pen-compatible :class:`.ttGlyph` object can -be chained together. +be chained together by calling he :meth:`.draw` method of each glyph +object, in turn, with a subsequent pen argument. -The majority of the pens are segment-oriented, meaning that they -operate by processing the Bezier segments of each glyph or glyph -component in order. This model corresponds to the way that glyph data -is stored in both TrueType-flavored and CFF-flavored OpenType -fonts. +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*. -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. UFO's -point-only file format can be deterministically converted to -segment-oriented form and vice-versa; therefore all pens are available -to be used with UFO sources. The generic example can be found on the -:ref:`pointPen` page: +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 - pointPen + basePen -but there are ``pointPen`` variants of several other pens, included -alongside the modules for their segment-oriented version. +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. -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 :ref:`basePen` page: +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: 1 + :maxdepth: 2 - basePen + pointPen -Some of the platform-specific pen modules rely on importing external -Python libraries; these cases are noted on the relevant pens' pages. +but there are also ``-PointPen`` variants available for several of the +other pens, included alongside the modules for their segment-oriented +version. @@ -60,9 +59,15 @@ 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: 1 + :maxdepth: 2 areaPen boundsPen @@ -82,7 +87,7 @@ Pen modules ttGlyphPen .. toctree:: - :maxdepth: 1 + :maxdepth: 2 cairoPen cocoaPen diff --git a/Doc/source/pens/pointPen.rst b/Doc/source/pens/pointPen.rst index 8c7026e376..055b995385 100644 --- a/Doc/source/pens/pointPen.rst +++ b/Doc/source/pens/pointPen.rst @@ -1,6 +1,11 @@ -######## -pointPen -######## +#################################### +pointPen: Point-oriented pen classes +#################################### + +.. currentmodule:: fontTools.pens.pointPen + +This module contains base classes for point-oriented :doc:`pens +`. .. automodule:: fontTools.pens.pointPen :members: From df23d628eca8217de19f83756d44a0446dd2e170 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 9 Dec 2024 21:27:25 -0700 Subject: [PATCH 019/105] [GVAR] Implement table Is exactly like `gvar` but uses a 24bit glyphCount. --- Doc/source/ttx.rst | 14 ++++++------- Lib/fontTools/ttLib/tables/G_V_A_R_.py | 5 +++++ Lib/fontTools/ttLib/tables/__init__.py | 1 + Lib/fontTools/ttLib/tables/_g_v_a_r.py | 28 ++++++++++++++++++++------ 4 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 Lib/fontTools/ttLib/tables/G_V_A_R_.py diff --git a/Doc/source/ttx.rst b/Doc/source/ttx.rst index 62b91b45d1..a711ef1c05 100644 --- a/Doc/source/ttx.rst +++ b/Doc/source/ttx.rst @@ -36,13 +36,13 @@ The TTX file format The following tables are currently supported:: BASE, CBDT, CBLC, CFF, CFF2, COLR, CPAL, DSIG, Debg, EBDT, EBLC, - FFTM, Feat, GDEF, GMAP, GPKG, GPOS, GSUB, Glat, Gloc, HVAR, JSTF, - LTSH, MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, TSI0, - TSI1, TSI2, TSI3, TSI5, TSIB, TSIC, TSID, TSIJ, TSIP, TSIS, TSIV, - TTFA, VARC, VDMX, VORG, VVAR, ankr, avar, bsln, cidg, cmap, cvar, - cvt, feat, fpgm, fvar, gasp, gcid, glyf, gvar, hdmx, head, hhea, - hmtx, kern, lcar, loca, ltag, maxp, meta, mort, morx, name, opbd, - post, prep, prop, sbix, trak, vhea and vmtx + FFTM, Feat, GDEF, GMAP, GPKG, GPOS, GSUB, GVAR, Glat, Gloc, HVAR, + JSTF, LTSH, MATH, META, MVAR, OS/2, SING, STAT, SVG, Silf, Sill, + TSI0, TSI1, TSI2, TSI3, TSI5, TSIB, TSIC, TSID, TSIJ, TSIP, TSIS, + TSIV, TTFA, VARC, VDMX, VORG, VVAR, ankr, avar, bsln, cidg, cmap, + cvar, cvt, feat, fpgm, fvar, gasp, gcid, glyf, gvar, hdmx, head, + hhea, hmtx, kern, lcar, loca, ltag, maxp, meta, mort, morx, name, + opbd, post, prep, prop, sbix, trak, vhea and vmtx .. end table list diff --git a/Lib/fontTools/ttLib/tables/G_V_A_R_.py b/Lib/fontTools/ttLib/tables/G_V_A_R_.py new file mode 100644 index 0000000000..889b1f2a3b --- /dev/null +++ b/Lib/fontTools/ttLib/tables/G_V_A_R_.py @@ -0,0 +1,5 @@ +from ._g_v_a_r import table__g_v_a_r + + +class table_G_V_A_R_(table__g_v_a_r): + gid_size = 3 diff --git a/Lib/fontTools/ttLib/tables/__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/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 39c1707a9d..9a7eccafea 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -23,19 +23,24 @@ # FreeType2 source code for parsing 'gvar': # http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c -GVAR_HEADER_FORMAT = """ +GVAR_HEADER_FORMAT_HEAD = """ > # big endian version: H reserved: H axisCount: H sharedTupleCount: H offsetToSharedTuples: I - glyphCount: H +""" +# In between the HEAD and TAIL lies the glyphCount, which is +# of different size: 2 bytes for gvar, and 3 bytes for GVAR. +GVAR_HEADER_FORMAT_TAIL = """ + > # big endian flags: H offsetToGlyphVariationData: I """ -GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT) +GVAR_HEADER_SIZE_HEAD = sstruct.calcsize(GVAR_HEADER_FORMAT_HEAD) +GVAR_HEADER_SIZE_TAIL = sstruct.calcsize(GVAR_HEADER_FORMAT_TAIL) class table__g_v_a_r(DefaultTable.DefaultTable): @@ -50,6 +55,7 @@ class table__g_v_a_r(DefaultTable.DefaultTable): """ dependencies = ["fvar", "glyf"] + gid_size = 2 def __init__(self, tag=None): DefaultTable.DefaultTable.__init__(self, tag) @@ -72,18 +78,22 @@ 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) + compiledHeader = b''.join([ + sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), + int.to_bytes(len(compiledGlyphs), self.gid_size, "big"), + sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), + ]) result = [compiledHeader, compiledOffsets] result.extend(sharedTuples) @@ -114,7 +124,13 @@ 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( From a4dcfe9d40cd05660506c470a9b0ee7633bce525 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 9 Dec 2024 21:29:36 -0700 Subject: [PATCH 020/105] [fontBuilder] Add setupGVAR() --- Lib/fontTools/fontBuilder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/fontTools/fontBuilder.py b/Lib/fontTools/fontBuilder.py index d4af38fba4..f8da717bab 100644 --- a/Lib/fontTools/fontBuilder.py +++ b/Lib/fontTools/fontBuilder.py @@ -714,6 +714,12 @@ def setupGvar(self, variations): gvar.reserved = 0 gvar.variations = variations + def setupGVAR(self, variations): + gvar = self.font["GVAR"] = newTable("GVAR") + gvar.version = 1 + gvar.reserved = 0 + gvar.variations = variations + def calcGlyphBounds(self): """Calculate the bounding boxes of all glyphs in the `glyf` table. This is usually not called explicitly by client code. From 9fa5164ca3977740460c73e7934ad861f414be7b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Mon, 9 Dec 2024 21:34:14 -0700 Subject: [PATCH 021/105] [GVAR] Change offsetToData size to 24bit As per https://github.com/harfbuzz/boring-expansion-spec/issues/162 --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 56 +++++++++++++++++++------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 9a7eccafea..bdd0d7d10d 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -89,11 +89,13 @@ def compile(self, ttFont): header["offsetToGlyphVariationData"] = ( header["offsetToSharedTuples"] + sharedTupleSize ) - compiledHeader = b''.join([ - sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), - int.to_bytes(len(compiledGlyphs), self.gid_size, "big"), - sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), - ]) + compiledHeader = b"".join( + [ + sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), + int.to_bytes(len(compiledGlyphs), self.gid_size, "big"), + sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), + ] + ) result = [compiledHeader, compiledOffsets] result.extend(sharedTuples) @@ -112,6 +114,7 @@ def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices): pointCountUnused = 0 # pointCount is actually unused by compileGlyph result.append( compileGlyph_( + self.gid_size, variations, pointCountUnused, axisTags, @@ -128,8 +131,14 @@ def decompile(self, data, ttFont): # 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) + 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 @@ -160,7 +169,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 @@ -278,23 +287,42 @@ def getNumPoints_(glyph): def compileGlyph_( - variations, pointCount, axisTags, sharedCoordIndices, *, optimizeSize=True + dataOffsetSize, + variations, + pointCount, + axisTags, + sharedCoordIndices, + *, + optimizeSize=True, ): + assert dataOffsetSize in (2, 3) tupleVariationCount, tuples, data = tv.compileTupleVariationStore( variations, pointCount, axisTags, sharedCoordIndices, optimizeSize=optimizeSize ) if tupleVariationCount == 0: return b"" - result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data] - if (len(tuples) + len(data)) % 2 != 0: + + offsetToData = 2 + dataOffsetSize + len(tuples) + + result = [ + tupleVariationCount.to_bytes(2, "big"), + offsetToData.to_bytes(dataOffsetSize, "big"), + tuples, + data, + ] + if (offsetToData + len(data)) % 2 != 0: result.append(b"\0") # padding return b"".join(result) -def decompileGlyph_(pointCount, sharedTuples, axisTags, data): - if len(data) < 4: +def decompileGlyph_(dataOffsetSize, pointCount, sharedTuples, axisTags, data): + assert dataOffsetSize in (2, 3) + if len(data) < 2 + dataOffsetSize: return [] - tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4]) + + tupleVariationCount = int.from_bytes(data[:2], "big") + offsetToData = int.from_bytes(data[2 : 2 + dataOffsetSize], "big") + dataPos = offsetToData return tv.decompileTupleVariationStore( "gvar", From 29fc63ee7edbeff2b7a604db63cdee840b709b8d Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Tue, 10 Dec 2024 21:59:13 -0700 Subject: [PATCH 022/105] [GVAR] Minor optimization --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index bdd0d7d10d..c5bdfa3259 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -89,15 +89,14 @@ def compile(self, ttFont): header["offsetToGlyphVariationData"] = ( header["offsetToSharedTuples"] + sharedTupleSize ) - compiledHeader = b"".join( - [ - sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), - int.to_bytes(len(compiledGlyphs), self.gid_size, "big"), - sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), - ] - ) - result = [compiledHeader, compiledOffsets] + result = [ + sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), + int.to_bytes(len(compiledGlyphs), self.gid_size, "big"), + sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), + ] + + result.append(compiledOffsets) result.extend(sharedTuples) result.extend(compiledGlyphs) return b"".join(result) From 550dd29cbb6564052d2278e0fcd12a869c69f049 Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 11 Dec 2024 01:27:30 -0700 Subject: [PATCH 023/105] [gvar] Minor, call method --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index c5bdfa3259..9e18051fd0 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -92,7 +92,7 @@ def compile(self, ttFont): result = [ sstruct.pack(GVAR_HEADER_FORMAT_HEAD, header), - int.to_bytes(len(compiledGlyphs), self.gid_size, "big"), + len(compiledGlyphs).to_bytes(self.gid_size, "big"), sstruct.pack(GVAR_HEADER_FORMAT_TAIL, header), ] From bb3bc94ff3d3cf725cad603c29e62a08774ffdfd Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Wed, 11 Dec 2024 01:28:50 -0700 Subject: [PATCH 024/105] [GVAR] Fix offset miscalculation --- Lib/fontTools/ttLib/tables/_g_v_a_r.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_g_v_a_r.py b/Lib/fontTools/ttLib/tables/_g_v_a_r.py index 9e18051fd0..7024a1f9c4 100644 --- a/Lib/fontTools/ttLib/tables/_g_v_a_r.py +++ b/Lib/fontTools/ttLib/tables/_g_v_a_r.py @@ -330,6 +330,6 @@ def decompileGlyph_(dataOffsetSize, pointCount, sharedTuples, axisTags, data): pointCount, sharedTuples, data, - 4, + 2 + dataOffsetSize, offsetToData, ) From 1586c94a00a15ee7536b1cb62613e35bedd58c5b Mon Sep 17 00:00:00 2001 From: Liang Hai Date: Sat, 8 Mar 2025 04:01:41 +0100 Subject: [PATCH 025/105] Correct ChainContextPosStatement and ChainContextSubstStatement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They don’t have self.glyph. --- Lib/fontTools/feaLib/ast.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 10c49058c4..eeb09e736a 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -753,7 +753,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 +811,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 From 8b47a2cb9f31b5cc42bd1e9adbe8bb1087ee48b9 Mon Sep 17 00:00:00 2001 From: Liang Hai Date: Sat, 8 Mar 2025 04:07:18 +0100 Subject: [PATCH 026/105] Add name as required by CONTRIBUTING.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit “Liang” is my surname. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b604ea7ca5..5daa2f5054 100644 --- a/README.rst +++ b/README.rst @@ -266,7 +266,7 @@ Vincent Connare, David Corbett, Simon Cozens, Dave Crossland, Simon Daniels, Peter Dekkers, Behdad Esfahbod, Behnam Esfahbod, Hannes Famira, Sam Fishman, Matt Fontaine, Takaaki Fuji, Rob Hagemans, Yannis Haralambous, Greg Hitchcock, Jeremie Hornus, Khaled Hosny, John Hudson, Denis Moyogo Jacquerye, Jack Jansen, -Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, Peter +Tom Kacvinsky, Jens Kutilek, Antoine Leca, Werner Lemberg, Tal Leming, Liang Hai, Peter Lofting, Cosimo Lupo, Olli Meier, Masaya Nakamura, Dave Opstad, Laurence Penney, Roozbeh Pournader, Garret Rieger, Read Roberts, Colin Rofls, Guido van Rossum, Just van Rossum, Andreas Seidel, Georg Seifert, Chris Simpkins, Miguel Sousa, From 28fb32e1a8b870eb694900a6747680785f952ea8 Mon Sep 17 00:00:00 2001 From: Jany Belluz Date: Thu, 3 Apr 2025 11:41:43 +0100 Subject: [PATCH 027/105] =?UTF-8?q?Bump=20version:=204.57.0=20=E2=86=92=20?= =?UTF-8?q?4.57.1.dev0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/fontTools/__init__.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index bc9f0e9055..c69b6736ee 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -3,6 +3,6 @@ log = logging.getLogger(__name__) -version = __version__ = "4.57.0" +version = __version__ = "4.57.1.dev0" __all__ = ["version", "log", "configLogger"] diff --git a/setup.cfg b/setup.cfg index 148357a191..249224ae61 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.57.0 +current_version = 4.57.1.dev0 commit = True tag = False tag_name = {new_version} diff --git a/setup.py b/setup.py index 64a49258b2..0e07b65680 100755 --- a/setup.py +++ b/setup.py @@ -495,7 +495,7 @@ def build_extensions(self): setup_params = dict( name="fonttools", - version="4.57.0", + version="4.57.1.dev0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com", From 3d14adc6b93b8d9dfeafbba17ea285d0fcd83dc6 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 9 Apr 2025 16:40:32 -0400 Subject: [PATCH 028/105] [ttLib] Apply rounding more often in getCoordinates Regardless of whether or not a component transform is simple, we still have to round the points before returning, or we risk the error propogating up to another component that might have a non-trivial transform. --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 6 +---- Tests/ttLib/tables/_g_l_y_f_test.py | 32 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index c05fcea5d3..38b1d72462 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1285,11 +1285,7 @@ def getCoordinates(self, glyfTable, *, round=noRound): # however, if the referenced component glyph is another composite, we # must not round here but only at the end, after all the nested # transforms have been applied, or else rounding errors will compound. - if ( - round is not noRound - and g.numberOfContours > 0 - and not compo._hasOnlyIntegerTranslate() - ): + if round is not noRound and g.numberOfContours > 0: coordinates.toInt(round=round) if hasattr(compo, "firstPt"): # component uses two reference points: we apply the transform _before_ diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 10e053c4d5..8e018c058f 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 @@ -538,6 +539,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 = {} From 6120962031be51d2578509378ce8a4be11ad0fce Mon Sep 17 00:00:00 2001 From: Harry Dalton Date: Wed, 9 Apr 2025 22:48:28 +0100 Subject: [PATCH 029/105] Prune unused regions from HVAR and VVAR when using a direct mapping (#3797) This saves a handful of bytes for the common case where a font is variable but has no variation in its advances (e.g. for some monospace fonts across `wght`), and we include an empty metrics variation table so that the shaper can immediately ascertain this without interpolating phantom points. --- Lib/fontTools/varLib/__init__.py | 2 + Tests/varLib/builder_test.py | 91 ++++++++++++++++++++ Tests/varLib/data/test_results/Build.ttx | 23 ++--- Tests/varLib/data/test_results/BuildMain.ttx | 23 ++--- 4 files changed, 109 insertions(+), 30 deletions(-) diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index f3d8d59dc5..e3c00d73fa 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -555,6 +555,8 @@ def _add_VHVAR(font, axisTags, tableFields, getAdvanceMetrics): varData.addItem(vhAdvanceDeltasAndSupports[glyphName][0], round=noRound) varData.optimize() directStore = builder.buildVarStore(varTupleList, [varData]) + # remove unused regions from VarRegionList + directStore.prune_regions() # Build optimized indirect mapping storeBuilder = varStore.OnlineVarStoreBuilder(axisTags) diff --git a/Tests/varLib/builder_test.py b/Tests/varLib/builder_test.py index 33d1dfb023..0885ec746c 100644 --- a/Tests/varLib/builder_test.py +++ b/Tests/varLib/builder_test.py @@ -1,3 +1,13 @@ +from io import StringIO +from fontTools.designspaceLib import ( + AxisDescriptor, + DesignSpaceDocument, + SourceDescriptor, +) +from fontTools.fontBuilder import FontBuilder +from fontTools.misc.xmlWriter import XMLWriter +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.varLib import build from fontTools.varLib.builder import buildVarData import pytest @@ -150,6 +160,87 @@ def test_buildVarData_optimize( assert data.Item == expected_items +def test_empty_vhvar_size(): + """HVAR/VHVAR should be present but empty when there are no glyph metrics + variations, and should use a direct mapping for optimal encoding.""" + + # Make a designspace that varies the outlines of 'A' but not its advance. + doc = DesignSpaceDocument() + + doc.addAxis( + AxisDescriptor(tag="wght", name="Weight", minimum=400, default=400, maximum=700) + ) + + for wght in (400, 700): + # Outlines depend on weight. + pen = TTGlyphPen(None) + pen.lineTo((0, wght)) + pen.lineTo((wght, wght)) + pen.lineTo((wght, 0)) + pen.closePath() + glyphs = {"A": pen.glyph()} + + fb = FontBuilder(unitsPerEm=1000) + fb.setupGlyphOrder(list(glyphs.keys())) + fb.setupGlyf(glyphs) + + # Horizontal advance does not vary. + fb.setupHorizontalMetrics( + {name: (500, fb.font["glyf"][name].xMin) for name in glyphs} # type: ignore + ) + fb.setupHorizontalHeader(ascent=1000, descent=0) + + # Vertical advance does not vary. + fb.setupVerticalMetrics( + {name: (500, 1000 - fb.font["glyf"][name].yMax) for name in glyphs} # type: ignore + ) + fb.setupVerticalHeader(ascent=1000, descent=0) + + fb.setupNameTable({"familyName": "TestEmptyVhvar", "styleName": "Regular"}) + fb.setupPost() + doc.addSource(SourceDescriptor(font=fb.font, location={"Weight": wght})) + + # Compile. + vf, *_ = build(doc) + + # Test both tables' encodings: + for table in ("HVAR", "VVAR"): + # Variable glyph metrics table should be built even when there are no + # glyph metrics variations. + assert table in vf + + # The table should be empty, and use a direct mapping for optimal size. + expected = """ + + + + + + + + + + + + + + + + +""".lstrip() + + with StringIO() as buffer: + writer = XMLWriter(buffer) + vf[table].toXML(writer, vf) + actual = buffer.getvalue() + assert actual == expected + + # The table should be encodable in at least this size. + # (VVAR has an extra Offset32 to point to a vertical origin mapping) + optimal_size = {"HVAR": 42, "VVAR": 46}[table] + assert len(vf[table].compile(vf)) <= optimal_size + + if __name__ == "__main__": import sys diff --git a/Tests/varLib/data/test_results/Build.ttx b/Tests/varLib/data/test_results/Build.ttx index f4fa0ae48e..c4f70e8aba 100644 --- a/Tests/varLib/data/test_results/Build.ttx +++ b/Tests/varLib/data/test_results/Build.ttx @@ -1,5 +1,5 @@ - + @@ -45,26 +45,19 @@ - + - - - - - + + + + + + - - - - - - - - diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx index 1c2c1fa8d2..c82323e425 100644 --- a/Tests/varLib/data/test_results/BuildMain.ttx +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -1,5 +1,5 @@ - + @@ -597,26 +597,19 @@ - + - - - - - + + + + + + - - - - - - - - From 8cf6dc9488fb9e1bc6ecca792d386dabff064836 Mon Sep 17 00:00:00 2001 From: Colin Rofls Date: Wed, 9 Apr 2025 18:31:51 -0400 Subject: [PATCH 030/105] [ttLib] Ignore component bounds if empty That is, if the bounds of a component have zero area, do not consider it when computing the bounds of the parent. This handles the case of a composite glyph where all the components are themselves empty, which if it sounds weird you're probably new around here. --- Lib/fontTools/ttLib/tables/_g_l_y_f.py | 2 +- Tests/ttLib/tables/_g_l_y_f_test.py | 64 ++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py index c05fcea5d3..7ee8ffa2f6 100644 --- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py +++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py @@ -1225,7 +1225,7 @@ def tryRecalcBoundsComposite(self, glyfTable, *, boundsDone=None): if boundsDone is not None: boundsDone.add(glyphName) # empty components shouldn't update the bounds of the parent glyph - if g.numberOfContours == 0: + if g.yMin == g.yMax and g.xMin == g.xMax: continue x, y = compo.x, compo.y diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 10e053c4d5..33120ff62f 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -567,17 +567,75 @@ def test_recalcBounds_empty_components(self): pen = TTGlyphPen(glyphSet) # empty simple glyph foo = glyphSet["foo"] = pen.glyph() + + pen.moveTo((5, 5)) + pen.lineTo((10, 10)) + pen.lineTo((10, 5)) + pen.lineTo((5, 5)) + pen.closePath() + + # non-empty simple glyph + doo = glyphSet["doo"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) # use the empty 'foo' glyph as a component in 'bar' with some x/y offsets pen.addComponent("foo", (1, 0, 0, 1, -80, 50)) + # and use the non-empty 'doo' glyph at origin + pen.addComponent("doo", (1, 0, 0, 1, 0, 0)) + bar = glyphSet["bar"] = pen.glyph() + + foo.recalcBounds(glyphSet) + doo.recalcBounds(glyphSet) + bar.recalcBounds(glyphSet) + + assert (foo.xMin, foo.yMin, foo.xMax, foo.yMax) == (0, 0, 0, 0) + assert (doo.xMin, doo.yMin, doo.xMax, doo.yMax) == (5, 5, 10, 10) + # the composite should have bounds identical to 'doo'; + # the empty foo glyph should do nothing + assert (bar.xMin, bar.yMin, bar.xMax, bar.yMax) == (5, 5, 10, 10) + + def test_recalcBounds_empty_components_nested(self): + # this differs from the above in an important way: it has a 'foofoo' + # glyph, which is a composite glyph, where none of its components + # have any contours; we want to check that when this glyph is referenced + # it is also treated as not contributing to the parent's bounds. + + glyphSet = {} + pen = TTGlyphPen(glyphSet) + # empty simple glyph + foo = glyphSet["foo"] = pen.glyph() + + pen.addComponent("foo", (1, 0, 0, 1, 20, 20)) + foofoo = glyphSet["foofoo"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) + + pen.moveTo((5, 5)) + pen.lineTo((10, 10)) + pen.lineTo((10, 5)) + pen.lineTo((5, 5)) + pen.closePath() + + # non-empty simple glyph + doo = glyphSet["doo"] = pen.glyph() + + pen = TTGlyphPen(glyphSet) + # use the empty 'foo' glyph as a component in 'bar' with some x/y offsets + pen.addComponent("foofoo", (1, 0, 0, 1, 0, 0)) + # and use the non-empty 'doo' glyph at origin + pen.addComponent("doo", (1, 0, 0, 1, 0, 0)) bar = glyphSet["bar"] = pen.glyph() foo.recalcBounds(glyphSet) + foofoo.recalcBounds(glyphSet) + doo.recalcBounds(glyphSet) bar.recalcBounds(glyphSet) - # we expect both the empty simple glyph and the composite referencing it - # to have empty bounding boxes (0, 0, 0, 0) no matter the component's shift assert (foo.xMin, foo.yMin, foo.xMax, foo.yMax) == (0, 0, 0, 0) - assert (bar.xMin, bar.yMin, bar.xMax, bar.yMax) == (0, 0, 0, 0) + assert (doo.xMin, doo.yMin, doo.xMax, doo.yMax) == (5, 5, 10, 10) + # the composite should have bounds identical to 'doo'; + # the empty foo glyph should do nothing + assert (bar.xMin, bar.yMin, bar.xMax, bar.yMax) == (5, 5, 10, 10) class GlyphComponentTest: From 53d57a162ed1f0c51730a7ceaa43bd7b7d198d62 Mon Sep 17 00:00:00 2001 From: Garret Rieger Date: Fri, 11 Apr 2025 23:52:37 +0000 Subject: [PATCH 031/105] [subset] close unicodes over bidi mirror variants. Matching a change to harfbuzz, for context: https://github.com/harfbuzz/harfbuzz/pull/5282 and https://github.com/harfbuzz/harfbuzz/issues/5281 --- Lib/fontTools/subset/__init__.py | 12 ++++++++++++ Lib/fontTools/unicodedata/__init__.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 8458edc359..2262d7594f 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -16,6 +16,8 @@ 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 +from fontTools.unicodedata import MIRRORED import sys import struct import array @@ -2870,6 +2872,15 @@ def prune_post_subset(self, font, options): def closure_glyphs(self, s): tables = [t for t in self.tables if t.isUnicode()] + # Closure unicodes, which for now is pulling in bidi mirrored variants + if s.options.bidi_closure: + additional_unicodes = set() + for u in s.unicodes_requested: + if mirrored(chr(u)): + mirror_u = ord(MIRRORED[chr(u)]) + additional_unicodes.add(mirror_u) + s.unicodes_requested.update(additional_unicodes) + # Close glyphs for table in tables: if table.format == 14: @@ -3191,6 +3202,7 @@ def __init__(self, **kwargs): self.font_number = -1 self.pretty_svg = False self.lazy = True + self.bidi_closure = True self.set(**kwargs) diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py index edae44ec71..43af928878 100644 --- a/Lib/fontTools/unicodedata/__init__.py +++ b/Lib/fontTools/unicodedata/__init__.py @@ -15,6 +15,8 @@ # fall back to built-in unicodedata (possibly outdated) from unicodedata import * +from bidi.mirror import MIRRORED + from . import Blocks, Scripts, ScriptExtensions, OTTags @@ -43,6 +45,7 @@ "script_horizontal_direction", "ot_tags_from_script", "ot_tag_to_script", + "MIRRORED", ] From d6060fa8602970e467a23b00c2b13463e456fe8e Mon Sep 17 00:00:00 2001 From: Garret Rieger Date: Mon, 14 Apr 2025 19:04:51 +0000 Subject: [PATCH 032/105] Embed the unicoding bidi mirrored mapping instead of importing python-bidi. --- Lib/fontTools/subset/__init__.py | 5 +- Lib/fontTools/unicodedata/__init__.py | 437 ++++++++++++++++++++++++- Tests/subset/data/expect_keep_math.ttx | 173 +++++++++- 3 files changed, 590 insertions(+), 25 deletions(-) diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 2262d7594f..6d63a3c944 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -16,7 +16,6 @@ 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 from fontTools.unicodedata import MIRRORED import sys import struct @@ -2876,8 +2875,8 @@ def closure_glyphs(self, s): if s.options.bidi_closure: additional_unicodes = set() for u in s.unicodes_requested: - if mirrored(chr(u)): - mirror_u = ord(MIRRORED[chr(u)]) + mirror_u = MIRRORED.get(u) + if mirror_u is not None: additional_unicodes.add(mirror_u) s.unicodes_requested.update(additional_unicodes) diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py index 43af928878..560a16bf8f 100644 --- a/Lib/fontTools/unicodedata/__init__.py +++ b/Lib/fontTools/unicodedata/__init__.py @@ -15,11 +15,8 @@ # fall back to built-in unicodedata (possibly outdated) from unicodedata import * -from bidi.mirror import MIRRORED - from . import Blocks, Scripts, ScriptExtensions, OTTags - __all__ = [ # names from built-in unicodedata module "lookup", @@ -45,9 +42,441 @@ "script_horizontal_direction", "ot_tags_from_script", "ot_tag_to_script", - "MIRRORED", ] +# Derived from http://www.unicode.org/Public/UNIDATA/BidiMirroring.txt +# cat BidiMirroring.txt | grep "^[0-9A-F]" | sed "s/;//" | awk '{print " 0x"$1": 0x"$2","}' +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, +} + def script(char): """Return the four-letter script code assigned to the Unicode character 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 @@ + + + From f0802fe2f432e9211c989d68fae100f5cb986aec Mon Sep 17 00:00:00 2001 From: Garret Rieger Date: Mon, 14 Apr 2025 23:25:31 +0000 Subject: [PATCH 033/105] [subset] Move mirrored data into it's own file. --- Lib/fontTools/subset/__init__.py | 4 +- Lib/fontTools/unicodedata/Mirrored.py | 446 ++++++++++++++++++++++++++ Lib/fontTools/unicodedata/__init__.py | 437 +------------------------ 3 files changed, 452 insertions(+), 435 deletions(-) create mode 100644 Lib/fontTools/unicodedata/Mirrored.py diff --git a/Lib/fontTools/subset/__init__.py b/Lib/fontTools/subset/__init__.py index 6d63a3c944..056ad81bab 100644 --- a/Lib/fontTools/subset/__init__.py +++ b/Lib/fontTools/subset/__init__.py @@ -16,7 +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 +from fontTools.unicodedata import mirrored import sys import struct import array @@ -2875,7 +2875,7 @@ def closure_glyphs(self, s): if s.options.bidi_closure: additional_unicodes = set() for u in s.unicodes_requested: - mirror_u = MIRRORED.get(u) + mirror_u = mirrored(u) if mirror_u is not None: additional_unicodes.add(mirror_u) s.unicodes_requested.update(additional_unicodes) diff --git a/Lib/fontTools/unicodedata/Mirrored.py b/Lib/fontTools/unicodedata/Mirrored.py new file mode 100644 index 0000000000..75b51a9012 --- /dev/null +++ b/Lib/fontTools/unicodedata/Mirrored.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +# +# NOTE: The mappings in this file were generated from the command line: +# cat BidiMirroring.txt | grep "^[0-9A-F]" | sed "s/;//" | awk '{print " 0x"$1": 0x"$2","}' +# +# Source: http://www.unicode.org/Public/UNIDATA/BidiMirroring.txt +# License: http://unicode.org/copyright.html#License +# +# BidiMirroring-16.0.0.txt +# Date: 2024-01-30 +# © 2024 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use and license, see https://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see https://www.unicode.org/reports/tr44/ +MIRRORED = { + 0x0028: 0x0029, + 0x0029: 0x0028, + 0x003C: 0x003E, + 0x003E: 0x003C, + 0x005B: 0x005D, + 0x005D: 0x005B, + 0x007B: 0x007D, + 0x007D: 0x007B, + 0x00AB: 0x00BB, + 0x00BB: 0x00AB, + 0x0F3A: 0x0F3B, + 0x0F3B: 0x0F3A, + 0x0F3C: 0x0F3D, + 0x0F3D: 0x0F3C, + 0x169B: 0x169C, + 0x169C: 0x169B, + 0x2039: 0x203A, + 0x203A: 0x2039, + 0x2045: 0x2046, + 0x2046: 0x2045, + 0x207D: 0x207E, + 0x207E: 0x207D, + 0x208D: 0x208E, + 0x208E: 0x208D, + 0x2208: 0x220B, + 0x2209: 0x220C, + 0x220A: 0x220D, + 0x220B: 0x2208, + 0x220C: 0x2209, + 0x220D: 0x220A, + 0x2215: 0x29F5, + 0x221F: 0x2BFE, + 0x2220: 0x29A3, + 0x2221: 0x299B, + 0x2222: 0x29A0, + 0x2224: 0x2AEE, + 0x223C: 0x223D, + 0x223D: 0x223C, + 0x2243: 0x22CD, + 0x2245: 0x224C, + 0x224C: 0x2245, + 0x2252: 0x2253, + 0x2253: 0x2252, + 0x2254: 0x2255, + 0x2255: 0x2254, + 0x2264: 0x2265, + 0x2265: 0x2264, + 0x2266: 0x2267, + 0x2267: 0x2266, + 0x2268: 0x2269, + 0x2269: 0x2268, + 0x226A: 0x226B, + 0x226B: 0x226A, + 0x226E: 0x226F, + 0x226F: 0x226E, + 0x2270: 0x2271, + 0x2271: 0x2270, + 0x2272: 0x2273, + 0x2273: 0x2272, + 0x2274: 0x2275, + 0x2275: 0x2274, + 0x2276: 0x2277, + 0x2277: 0x2276, + 0x2278: 0x2279, + 0x2279: 0x2278, + 0x227A: 0x227B, + 0x227B: 0x227A, + 0x227C: 0x227D, + 0x227D: 0x227C, + 0x227E: 0x227F, + 0x227F: 0x227E, + 0x2280: 0x2281, + 0x2281: 0x2280, + 0x2282: 0x2283, + 0x2283: 0x2282, + 0x2284: 0x2285, + 0x2285: 0x2284, + 0x2286: 0x2287, + 0x2287: 0x2286, + 0x2288: 0x2289, + 0x2289: 0x2288, + 0x228A: 0x228B, + 0x228B: 0x228A, + 0x228F: 0x2290, + 0x2290: 0x228F, + 0x2291: 0x2292, + 0x2292: 0x2291, + 0x2298: 0x29B8, + 0x22A2: 0x22A3, + 0x22A3: 0x22A2, + 0x22A6: 0x2ADE, + 0x22A8: 0x2AE4, + 0x22A9: 0x2AE3, + 0x22AB: 0x2AE5, + 0x22B0: 0x22B1, + 0x22B1: 0x22B0, + 0x22B2: 0x22B3, + 0x22B3: 0x22B2, + 0x22B4: 0x22B5, + 0x22B5: 0x22B4, + 0x22B6: 0x22B7, + 0x22B7: 0x22B6, + 0x22B8: 0x27DC, + 0x22C9: 0x22CA, + 0x22CA: 0x22C9, + 0x22CB: 0x22CC, + 0x22CC: 0x22CB, + 0x22CD: 0x2243, + 0x22D0: 0x22D1, + 0x22D1: 0x22D0, + 0x22D6: 0x22D7, + 0x22D7: 0x22D6, + 0x22D8: 0x22D9, + 0x22D9: 0x22D8, + 0x22DA: 0x22DB, + 0x22DB: 0x22DA, + 0x22DC: 0x22DD, + 0x22DD: 0x22DC, + 0x22DE: 0x22DF, + 0x22DF: 0x22DE, + 0x22E0: 0x22E1, + 0x22E1: 0x22E0, + 0x22E2: 0x22E3, + 0x22E3: 0x22E2, + 0x22E4: 0x22E5, + 0x22E5: 0x22E4, + 0x22E6: 0x22E7, + 0x22E7: 0x22E6, + 0x22E8: 0x22E9, + 0x22E9: 0x22E8, + 0x22EA: 0x22EB, + 0x22EB: 0x22EA, + 0x22EC: 0x22ED, + 0x22ED: 0x22EC, + 0x22F0: 0x22F1, + 0x22F1: 0x22F0, + 0x22F2: 0x22FA, + 0x22F3: 0x22FB, + 0x22F4: 0x22FC, + 0x22F6: 0x22FD, + 0x22F7: 0x22FE, + 0x22FA: 0x22F2, + 0x22FB: 0x22F3, + 0x22FC: 0x22F4, + 0x22FD: 0x22F6, + 0x22FE: 0x22F7, + 0x2308: 0x2309, + 0x2309: 0x2308, + 0x230A: 0x230B, + 0x230B: 0x230A, + 0x2329: 0x232A, + 0x232A: 0x2329, + 0x2768: 0x2769, + 0x2769: 0x2768, + 0x276A: 0x276B, + 0x276B: 0x276A, + 0x276C: 0x276D, + 0x276D: 0x276C, + 0x276E: 0x276F, + 0x276F: 0x276E, + 0x2770: 0x2771, + 0x2771: 0x2770, + 0x2772: 0x2773, + 0x2773: 0x2772, + 0x2774: 0x2775, + 0x2775: 0x2774, + 0x27C3: 0x27C4, + 0x27C4: 0x27C3, + 0x27C5: 0x27C6, + 0x27C6: 0x27C5, + 0x27C8: 0x27C9, + 0x27C9: 0x27C8, + 0x27CB: 0x27CD, + 0x27CD: 0x27CB, + 0x27D5: 0x27D6, + 0x27D6: 0x27D5, + 0x27DC: 0x22B8, + 0x27DD: 0x27DE, + 0x27DE: 0x27DD, + 0x27E2: 0x27E3, + 0x27E3: 0x27E2, + 0x27E4: 0x27E5, + 0x27E5: 0x27E4, + 0x27E6: 0x27E7, + 0x27E7: 0x27E6, + 0x27E8: 0x27E9, + 0x27E9: 0x27E8, + 0x27EA: 0x27EB, + 0x27EB: 0x27EA, + 0x27EC: 0x27ED, + 0x27ED: 0x27EC, + 0x27EE: 0x27EF, + 0x27EF: 0x27EE, + 0x2983: 0x2984, + 0x2984: 0x2983, + 0x2985: 0x2986, + 0x2986: 0x2985, + 0x2987: 0x2988, + 0x2988: 0x2987, + 0x2989: 0x298A, + 0x298A: 0x2989, + 0x298B: 0x298C, + 0x298C: 0x298B, + 0x298D: 0x2990, + 0x298E: 0x298F, + 0x298F: 0x298E, + 0x2990: 0x298D, + 0x2991: 0x2992, + 0x2992: 0x2991, + 0x2993: 0x2994, + 0x2994: 0x2993, + 0x2995: 0x2996, + 0x2996: 0x2995, + 0x2997: 0x2998, + 0x2998: 0x2997, + 0x299B: 0x2221, + 0x29A0: 0x2222, + 0x29A3: 0x2220, + 0x29A4: 0x29A5, + 0x29A5: 0x29A4, + 0x29A8: 0x29A9, + 0x29A9: 0x29A8, + 0x29AA: 0x29AB, + 0x29AB: 0x29AA, + 0x29AC: 0x29AD, + 0x29AD: 0x29AC, + 0x29AE: 0x29AF, + 0x29AF: 0x29AE, + 0x29B8: 0x2298, + 0x29C0: 0x29C1, + 0x29C1: 0x29C0, + 0x29C4: 0x29C5, + 0x29C5: 0x29C4, + 0x29CF: 0x29D0, + 0x29D0: 0x29CF, + 0x29D1: 0x29D2, + 0x29D2: 0x29D1, + 0x29D4: 0x29D5, + 0x29D5: 0x29D4, + 0x29D8: 0x29D9, + 0x29D9: 0x29D8, + 0x29DA: 0x29DB, + 0x29DB: 0x29DA, + 0x29E8: 0x29E9, + 0x29E9: 0x29E8, + 0x29F5: 0x2215, + 0x29F8: 0x29F9, + 0x29F9: 0x29F8, + 0x29FC: 0x29FD, + 0x29FD: 0x29FC, + 0x2A2B: 0x2A2C, + 0x2A2C: 0x2A2B, + 0x2A2D: 0x2A2E, + 0x2A2E: 0x2A2D, + 0x2A34: 0x2A35, + 0x2A35: 0x2A34, + 0x2A3C: 0x2A3D, + 0x2A3D: 0x2A3C, + 0x2A64: 0x2A65, + 0x2A65: 0x2A64, + 0x2A79: 0x2A7A, + 0x2A7A: 0x2A79, + 0x2A7B: 0x2A7C, + 0x2A7C: 0x2A7B, + 0x2A7D: 0x2A7E, + 0x2A7E: 0x2A7D, + 0x2A7F: 0x2A80, + 0x2A80: 0x2A7F, + 0x2A81: 0x2A82, + 0x2A82: 0x2A81, + 0x2A83: 0x2A84, + 0x2A84: 0x2A83, + 0x2A85: 0x2A86, + 0x2A86: 0x2A85, + 0x2A87: 0x2A88, + 0x2A88: 0x2A87, + 0x2A89: 0x2A8A, + 0x2A8A: 0x2A89, + 0x2A8B: 0x2A8C, + 0x2A8C: 0x2A8B, + 0x2A8D: 0x2A8E, + 0x2A8E: 0x2A8D, + 0x2A8F: 0x2A90, + 0x2A90: 0x2A8F, + 0x2A91: 0x2A92, + 0x2A92: 0x2A91, + 0x2A93: 0x2A94, + 0x2A94: 0x2A93, + 0x2A95: 0x2A96, + 0x2A96: 0x2A95, + 0x2A97: 0x2A98, + 0x2A98: 0x2A97, + 0x2A99: 0x2A9A, + 0x2A9A: 0x2A99, + 0x2A9B: 0x2A9C, + 0x2A9C: 0x2A9B, + 0x2A9D: 0x2A9E, + 0x2A9E: 0x2A9D, + 0x2A9F: 0x2AA0, + 0x2AA0: 0x2A9F, + 0x2AA1: 0x2AA2, + 0x2AA2: 0x2AA1, + 0x2AA6: 0x2AA7, + 0x2AA7: 0x2AA6, + 0x2AA8: 0x2AA9, + 0x2AA9: 0x2AA8, + 0x2AAA: 0x2AAB, + 0x2AAB: 0x2AAA, + 0x2AAC: 0x2AAD, + 0x2AAD: 0x2AAC, + 0x2AAF: 0x2AB0, + 0x2AB0: 0x2AAF, + 0x2AB1: 0x2AB2, + 0x2AB2: 0x2AB1, + 0x2AB3: 0x2AB4, + 0x2AB4: 0x2AB3, + 0x2AB5: 0x2AB6, + 0x2AB6: 0x2AB5, + 0x2AB7: 0x2AB8, + 0x2AB8: 0x2AB7, + 0x2AB9: 0x2ABA, + 0x2ABA: 0x2AB9, + 0x2ABB: 0x2ABC, + 0x2ABC: 0x2ABB, + 0x2ABD: 0x2ABE, + 0x2ABE: 0x2ABD, + 0x2ABF: 0x2AC0, + 0x2AC0: 0x2ABF, + 0x2AC1: 0x2AC2, + 0x2AC2: 0x2AC1, + 0x2AC3: 0x2AC4, + 0x2AC4: 0x2AC3, + 0x2AC5: 0x2AC6, + 0x2AC6: 0x2AC5, + 0x2AC7: 0x2AC8, + 0x2AC8: 0x2AC7, + 0x2AC9: 0x2ACA, + 0x2ACA: 0x2AC9, + 0x2ACB: 0x2ACC, + 0x2ACC: 0x2ACB, + 0x2ACD: 0x2ACE, + 0x2ACE: 0x2ACD, + 0x2ACF: 0x2AD0, + 0x2AD0: 0x2ACF, + 0x2AD1: 0x2AD2, + 0x2AD2: 0x2AD1, + 0x2AD3: 0x2AD4, + 0x2AD4: 0x2AD3, + 0x2AD5: 0x2AD6, + 0x2AD6: 0x2AD5, + 0x2ADE: 0x22A6, + 0x2AE3: 0x22A9, + 0x2AE4: 0x22A8, + 0x2AE5: 0x22AB, + 0x2AEC: 0x2AED, + 0x2AED: 0x2AEC, + 0x2AEE: 0x2224, + 0x2AF7: 0x2AF8, + 0x2AF8: 0x2AF7, + 0x2AF9: 0x2AFA, + 0x2AFA: 0x2AF9, + 0x2BFE: 0x221F, + 0x2E02: 0x2E03, + 0x2E03: 0x2E02, + 0x2E04: 0x2E05, + 0x2E05: 0x2E04, + 0x2E09: 0x2E0A, + 0x2E0A: 0x2E09, + 0x2E0C: 0x2E0D, + 0x2E0D: 0x2E0C, + 0x2E1C: 0x2E1D, + 0x2E1D: 0x2E1C, + 0x2E20: 0x2E21, + 0x2E21: 0x2E20, + 0x2E22: 0x2E23, + 0x2E23: 0x2E22, + 0x2E24: 0x2E25, + 0x2E25: 0x2E24, + 0x2E26: 0x2E27, + 0x2E27: 0x2E26, + 0x2E28: 0x2E29, + 0x2E29: 0x2E28, + 0x2E55: 0x2E56, + 0x2E56: 0x2E55, + 0x2E57: 0x2E58, + 0x2E58: 0x2E57, + 0x2E59: 0x2E5A, + 0x2E5A: 0x2E59, + 0x2E5B: 0x2E5C, + 0x2E5C: 0x2E5B, + 0x3008: 0x3009, + 0x3009: 0x3008, + 0x300A: 0x300B, + 0x300B: 0x300A, + 0x300C: 0x300D, + 0x300D: 0x300C, + 0x300E: 0x300F, + 0x300F: 0x300E, + 0x3010: 0x3011, + 0x3011: 0x3010, + 0x3014: 0x3015, + 0x3015: 0x3014, + 0x3016: 0x3017, + 0x3017: 0x3016, + 0x3018: 0x3019, + 0x3019: 0x3018, + 0x301A: 0x301B, + 0x301B: 0x301A, + 0xFE59: 0xFE5A, + 0xFE5A: 0xFE59, + 0xFE5B: 0xFE5C, + 0xFE5C: 0xFE5B, + 0xFE5D: 0xFE5E, + 0xFE5E: 0xFE5D, + 0xFE64: 0xFE65, + 0xFE65: 0xFE64, + 0xFF08: 0xFF09, + 0xFF09: 0xFF08, + 0xFF1C: 0xFF1E, + 0xFF1E: 0xFF1C, + 0xFF3B: 0xFF3D, + 0xFF3D: 0xFF3B, + 0xFF5B: 0xFF5D, + 0xFF5D: 0xFF5B, + 0xFF5F: 0xFF60, + 0xFF60: 0xFF5F, + 0xFF62: 0xFF63, + 0xFF63: 0xFF62, +} diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py index 560a16bf8f..11fb1dfad6 100644 --- a/Lib/fontTools/unicodedata/__init__.py +++ b/Lib/fontTools/unicodedata/__init__.py @@ -15,7 +15,7 @@ # fall back to built-in unicodedata (possibly outdated) from unicodedata import * -from . import Blocks, Scripts, ScriptExtensions, OTTags +from . import Blocks, Mirrored, Scripts, ScriptExtensions, OTTags __all__ = [ # names from built-in unicodedata module @@ -44,438 +44,9 @@ "ot_tag_to_script", ] -# Derived from http://www.unicode.org/Public/UNIDATA/BidiMirroring.txt -# cat BidiMirroring.txt | grep "^[0-9A-F]" | sed "s/;//" | awk '{print " 0x"$1": 0x"$2","}' -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, -} + +def mirrored(code): + return Mirrored.MIRRORED.get(code) def script(char): From d2998c6e7965e61bebf315f3f50083ea3cc2ab84 Mon Sep 17 00:00:00 2001 From: Garret Rieger Date: Tue, 15 Apr 2025 17:02:37 +0000 Subject: [PATCH 034/105] Add doc string for mirrored(). --- Lib/fontTools/unicodedata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/fontTools/unicodedata/__init__.py b/Lib/fontTools/unicodedata/__init__.py index 11fb1dfad6..1adb07d289 100644 --- a/Lib/fontTools/unicodedata/__init__.py +++ b/Lib/fontTools/unicodedata/__init__.py @@ -46,6 +46,7 @@ def mirrored(code): + """If code (unicode codepoint) has a mirrored version returns it, otherwise None.""" return Mirrored.MIRRORED.get(code) From b3c8ada77ae60aaab631c243bdf2b63c75f05d9c Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Wed, 16 Apr 2025 09:28:37 +0200 Subject: [PATCH 035/105] Format with black --- Lib/fontTools/ttLib/tables/T_S_I__0.py | 85 ++++++++++++++------------ Lib/fontTools/ttLib/tables/T_S_I__5.py | 69 +++++++++++---------- 2 files changed, 83 insertions(+), 71 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/T_S_I__0.py b/Lib/fontTools/ttLib/tables/T_S_I__0.py index e4e6a32af9..4a31fc5cbc 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__0.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__0.py @@ -1,54 +1,59 @@ -""" 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. """ -from . import DefaultTable + import struct -tsi0Format = '>HHL' +from . import DefaultTable + +tsi0Format = ">HHL" + def fixlongs(glyphID, textLength, textOffset): - return int(glyphID), int(textLength), textOffset + return int(glyphID), int(textLength), textOffset class table_T_S_I__0(DefaultTable.DefaultTable): - dependencies = ["TSI1"] - - def decompile(self, data, ttFont): - indices = [] - size = struct.calcsize(tsi0Format) - numEntries = len(data) // size - for i in range(numEntries): - glyphID, textLength, textOffset = fixlongs(*struct.unpack(tsi0Format, data[:size])) - indices.append((glyphID, textLength, textOffset)) - data = data[size:] - assert len(data) == 0 - assert indices[-5] == (0XFFFE, 0, 0xABFC1F34), "bad magic number" - self.indices = indices[:-5] - self.extra_indices = indices[-4:] - - def compile(self, ttFont): - if not hasattr(self, "indices"): - # We have no corresponding table (TSI1 or TSI3); let's return - # no data, which effectively means "ignore us". - return b"" - data = b"" - for index, textLength, textOffset in self.indices: - data = data + struct.pack(tsi0Format, index, textLength, textOffset) - data = data + struct.pack(tsi0Format, 0XFFFE, 0, 0xABFC1F34) - for index, textLength, textOffset in self.extra_indices: - data = data + struct.pack(tsi0Format, index, textLength, textOffset) - return data - - def set(self, indices, extra_indices): - # gets called by 'TSI1' or 'TSI3' - self.indices = indices - self.extra_indices = extra_indices - - def toXML(self, writer, ttFont): - writer.comment("This table will be calculated by the compiler") - writer.newline() + dependencies = ["TSI1"] + + def decompile(self, data, ttFont): + indices = [] + size = struct.calcsize(tsi0Format) + numEntries = len(data) // size + for i in range(numEntries): + glyphID, textLength, textOffset = fixlongs( + *struct.unpack(tsi0Format, data[:size]) + ) + indices.append((glyphID, textLength, textOffset)) + data = data[size:] + assert len(data) == 0 + assert indices[-5] == (0xFFFE, 0, 0xABFC1F34), "bad magic number" + self.indices = indices[:-5] + self.extra_indices = indices[-4:] + + def compile(self, ttFont): + if not hasattr(self, "indices"): + # We have no corresponding table (TSI1 or TSI3); let's return + # no data, which effectively means "ignore us". + return b"" + data = b"" + for index, textLength, textOffset in self.indices: + data = data + struct.pack(tsi0Format, index, textLength, textOffset) + data = data + struct.pack(tsi0Format, 0xFFFE, 0, 0xABFC1F34) + for index, textLength, textOffset in self.extra_indices: + data = data + struct.pack(tsi0Format, index, textLength, textOffset) + return data + + def set(self, indices, extra_indices): + # gets called by 'TSI1' or 'TSI3' + self.indices = indices + self.extra_indices = extra_indices + + def toXML(self, writer, ttFont): + writer.comment("This table will be calculated by the compiler") + writer.newline() diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py index 1f7f63b192..fcc6fd70e3 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__5.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py @@ -1,41 +1,48 @@ -""" 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. """ + +import array +import sys + from fontTools.misc.textTools import safeEval + from . import DefaultTable -import sys -import array class table_T_S_I__5(DefaultTable.DefaultTable): - def decompile(self, data, ttFont): - a = array.array("H") - a.frombytes(data) - if sys.byteorder != "big": a.byteswap() - self.glyphGrouping = {} - for i in range(len(data) // 2): - self.glyphGrouping[ttFont.getGlyphName(i)] = a[i] - - def compile(self, ttFont): - glyphNames = ttFont.getGlyphOrder() - a = array.array("H") - for i in range(len(glyphNames)): - a.append(self.glyphGrouping.get(glyphNames[i], 0)) - if sys.byteorder != "big": a.byteswap() - return a.tobytes() - - def toXML(self, writer, ttFont): - names = sorted(self.glyphGrouping.keys()) - for glyphName in names: - writer.simpletag("glyphgroup", name=glyphName, value=self.glyphGrouping[glyphName]) - writer.newline() - - def fromXML(self, name, attrs, content, ttFont): - if not hasattr(self, "glyphGrouping"): - self.glyphGrouping = {} - if name != "glyphgroup": - return - self.glyphGrouping[attrs["name"]] = safeEval(attrs["value"]) + def decompile(self, data, ttFont): + a = array.array("H") + a.frombytes(data) + if sys.byteorder != "big": + a.byteswap() + self.glyphGrouping = {} + for i in range(len(data) // 2): + self.glyphGrouping[ttFont.getGlyphName(i)] = a[i] + + def compile(self, ttFont): + glyphNames = ttFont.getGlyphOrder() + a = array.array("H") + for i in range(len(glyphNames)): + a.append(self.glyphGrouping.get(glyphNames[i], 0)) + if sys.byteorder != "big": + a.byteswap() + return a.tobytes() + + def toXML(self, writer, ttFont): + names = sorted(self.glyphGrouping.keys()) + for glyphName in names: + writer.simpletag( + "glyphgroup", name=glyphName, value=self.glyphGrouping[glyphName] + ) + writer.newline() + + def fromXML(self, name, attrs, content, ttFont): + if not hasattr(self, "glyphGrouping"): + self.glyphGrouping = {} + if name != "glyphgroup": + return + self.glyphGrouping[attrs["name"]] = safeEval(attrs["value"]) From 07830f37115a3d97fb266a2d6b5d35ca1f24eac5 Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Wed, 16 Apr 2025 11:22:07 +0200 Subject: [PATCH 036/105] Allow compiling empty fpgm, prep, cvt tables --- Lib/fontTools/ttLib/tables/_c_v_t.py | 2 ++ Lib/fontTools/ttLib/tables/_f_p_g_m.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/ttLib/tables/_c_v_t.py b/Lib/fontTools/ttLib/tables/_c_v_t.py index 92c50a1b8d..51e2f78df8 100644 --- a/Lib/fontTools/ttLib/tables/_c_v_t.py +++ b/Lib/fontTools/ttLib/tables/_c_v_t.py @@ -21,6 +21,8 @@ def decompile(self, data, ttFont): self.values = values def compile(self, ttFont): + if not hasattr(self, "values"): + return b"" values = self.values[:] if sys.byteorder != "big": values.byteswap() diff --git a/Lib/fontTools/ttLib/tables/_f_p_g_m.py b/Lib/fontTools/ttLib/tables/_f_p_g_m.py index ba8e0488de..c21a9d4b68 100644 --- a/Lib/fontTools/ttLib/tables/_f_p_g_m.py +++ b/Lib/fontTools/ttLib/tables/_f_p_g_m.py @@ -20,7 +20,9 @@ def decompile(self, data, ttFont): self.program = program def compile(self, ttFont): - return self.program.getBytecode() + if hasattr(self, "program"): + return self.program.getBytecode() + return b"" def toXML(self, writer, ttFont): self.program.toXML(writer, ttFont) From 4a1c56f7002face24c61692e085048612973e259 Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Wed, 16 Apr 2025 12:03:38 +0200 Subject: [PATCH 037/105] Add tests for empty cvt and fpgm tables --- Tests/ttLib/tables/_c_v_t_test.py | 15 +++++++++++++++ Tests/ttLib/tables/_f_p_g_m_test.py | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Tests/ttLib/tables/_c_v_t_test.py 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) From 9af989f5e41dfa6235d0055c154248e2318db2c9 Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Wed, 16 Apr 2025 18:35:51 +0200 Subject: [PATCH 038/105] Log a warning when the number of entries doesn't match the number of glyphs --- Lib/fontTools/ttLib/tables/T_S_I__0.py | 12 +++++++++++- Lib/fontTools/ttLib/tables/T_S_I__5.py | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/ttLib/tables/T_S_I__0.py b/Lib/fontTools/ttLib/tables/T_S_I__0.py index 215392e5d9..d60e783c60 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__0.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__0.py @@ -8,10 +8,13 @@ See also https://learn.microsoft.com/en-us/typography/tools/vtt/tsi-tables """ +import logging import struct from . import DefaultTable +log = logging.getLogger(__name__) + tsi0Format = ">HHL" @@ -23,10 +26,17 @@ class table_T_S_I__0(DefaultTable.DefaultTable): dependencies = ["TSI1"] def decompile(self, data, ttFont): + numGlyphs = ttFont["maxp"].numGlyphs indices = [] size = struct.calcsize(tsi0Format) numEntries = len(data) // size - for i in range(numEntries): + if numEntries != numGlyphs + 5: + diff = numEntries - numGlyphs - 5 + log.warning( + "Number of glyphPrograms differs from the number of glyphs in the font " + f"by {abs(diff)} ({numEntries - 5} programs vs. {numGlyphs} glyphs)." + ) + for _ in range(numEntries): glyphID, textLength, textOffset = fixlongs( *struct.unpack(tsi0Format, data[:size]) ) diff --git a/Lib/fontTools/ttLib/tables/T_S_I__5.py b/Lib/fontTools/ttLib/tables/T_S_I__5.py index 5243c61dac..6afd76832f 100644 --- a/Lib/fontTools/ttLib/tables/T_S_I__5.py +++ b/Lib/fontTools/ttLib/tables/T_S_I__5.py @@ -7,21 +7,32 @@ """ import array +import logging import sys from fontTools.misc.textTools import safeEval from . import DefaultTable +log = logging.getLogger(__name__) + class table_T_S_I__5(DefaultTable.DefaultTable): def decompile(self, data, ttFont): + numGlyphs = ttFont["maxp"].numGlyphs a = array.array("H") a.frombytes(data) if sys.byteorder != "big": a.byteswap() self.glyphGrouping = {} - for i in range(len(data) // 2): + 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): From f165725667521c8d3d70c8600f1847e3dcf281d9 Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Wed, 16 Apr 2025 18:36:20 +0200 Subject: [PATCH 039/105] Sort imports --- Tests/ttLib/tables/T_S_I__0_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ttLib/tables/T_S_I__0_test.py b/Tests/ttLib/tables/T_S_I__0_test.py index 871ece3d6e..930ad0ae9a 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)] From 4032bfce38d191309f4223e873b0f29c13c08161 Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Wed, 16 Apr 2025 18:36:47 +0200 Subject: [PATCH 040/105] Add tests for mismatching entry and glyph counts --- Tests/ttLib/tables/T_S_I__0_test.py | 21 ++++++++ Tests/ttLib/tables/T_S_I__5_test.py | 77 +++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 Tests/ttLib/tables/T_S_I__5_test.py diff --git a/Tests/ttLib/tables/T_S_I__0_test.py b/Tests/ttLib/tables/T_S_I__0_test.py index 930ad0ae9a..dc739e1d4c 100644 --- a/Tests/ttLib/tables/T_S_I__0_test.py +++ b/Tests/ttLib/tables/T_S_I__0_test.py @@ -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)) From 8a4f4126b05a6a4286da5f0a8d782b1b680d6908 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Fri, 11 Apr 2025 22:33:45 +0200 Subject: [PATCH 041/105] [feaLib] Support subtable statements in CursivePos lookups --- Lib/fontTools/otlLib/builder.py | 16 +++- Tests/feaLib/builder_test.py | 1 + Tests/feaLib/data/CursivePosSubtable.fea | 9 +++ Tests/feaLib/data/CursivePosSubtable.ttx | 96 ++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 Tests/feaLib/data/CursivePosSubtable.fea create mode 100644 Tests/feaLib/data/CursivePosSubtable.ttx diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index b944ea8c26..6cbf616fad 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -949,8 +949,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): diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 402b5c41c0..cd7c36a843 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -87,6 +87,7 @@ class BuilderTest(unittest.TestCase): contextual_inline_multi_sub_format_2 contextual_inline_format_4 duplicate_language_stmt + CursivePosSubtable """.split() VARFONT_AXES = [ diff --git a/Tests/feaLib/data/CursivePosSubtable.fea b/Tests/feaLib/data/CursivePosSubtable.fea new file mode 100644 index 0000000000..89fb09c79d --- /dev/null +++ b/Tests/feaLib/data/CursivePosSubtable.fea @@ -0,0 +1,9 @@ +feature test { + pos cursive A ; + subtable; + pos cursive a ; + subtable; + pos cursive B ; + subtable; + pos cursive b ; +} test; diff --git a/Tests/feaLib/data/CursivePosSubtable.ttx b/Tests/feaLib/data/CursivePosSubtable.ttx new file mode 100644 index 0000000000..3828d21bc1 --- /dev/null +++ b/Tests/feaLib/data/CursivePosSubtable.ttx @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From bc39ca1112e2eb973555e261f334cb2f6aab837a Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Fri, 11 Apr 2025 23:16:36 +0200 Subject: [PATCH 042/105] [feaLib] Support subtable statements in MarkBasePos lookups --- Lib/fontTools/otlLib/builder.py | 53 ++++--- Tests/feaLib/builder_test.py | 1 + Tests/feaLib/data/MarkBasePosSubtable.fea | 18 +++ Tests/feaLib/data/MarkBasePosSubtable.ttx | 174 ++++++++++++++++++++++ 4 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 Tests/feaLib/data/MarkBasePosSubtable.fea create mode 100644 Tests/feaLib/data/MarkBasePosSubtable.ttx diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 6cbf616fad..900f24b5d4 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -997,17 +997,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): @@ -1017,26 +1025,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. diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index cd7c36a843..5b8aa798f7 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -88,6 +88,7 @@ class BuilderTest(unittest.TestCase): contextual_inline_format_4 duplicate_language_stmt CursivePosSubtable + MarkBasePosSubtable """.split() VARFONT_AXES = [ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 8fed9ec018b8074b8f52159a403c9ac765b54589 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 12 Apr 2025 00:26:42 +0200 Subject: [PATCH 043/105] [feaLib] Remove now unused indirection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the last commit, buildMarkBasePos() is now unused, but it didn’t do anything anyway, it simply called buildMarkBasePosSubtable() and had a comment about a possible future optimization that was never done in 9+ years. --- Lib/fontTools/otlLib/builder.py | 52 ++++----------------------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 900f24b5d4..4bd5f4d112 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1927,18 +1927,11 @@ def buildMarkArray(marks, glyphMap): return self -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. +def buildMarkBasePosSubtable(marks, bases, glyphMap): + """Build a single MarkBasePos (GPOS4) subtable. - Note that if you are implementing a layout compiler, you may find it more - flexible to use - :py:class:`fontTools.otlLib.lookupBuilders.MarkBasePosBuilder` instead. + This builds a mark-to-base lookup subtable containing all of the referenced + marks and bases. Example:: @@ -1946,42 +1939,7 @@ def buildMarkBasePos(marks, bases, glyphMap): 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. - """ - # 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)] - - -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`. + markbaseposes = [buildMarkBasePosSubtable(marks, bases, font.getReverseGlyphMap())] Args: marks (dict): A dictionary mapping anchors to glyphs; the keys being From 7a5a0268eddfd4646ddef95c843885e2d454a8b6 Mon Sep 17 00:00:00 2001 From: Luke Hamburg <1992842+luckman212@users.noreply.github.com> Date: Mon, 21 Oct 2024 09:08:11 -0400 Subject: [PATCH 044/105] Update shebang Shebang was hardcoded to /usr/bin/python which will fail on many modern OSes. --- Lib/fontTools/mtiLib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py index dbedf275e3..8896f24463 100644 --- a/Lib/fontTools/mtiLib/__init__.py +++ b/Lib/fontTools/mtiLib/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # FontDame-to-FontTools for OpenType Layout tables # From f72e13cbd8522eaa9664ee370e1f74f0a7e066b5 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 00:56:13 +0200 Subject: [PATCH 045/105] [mtiLib] Remove unneeded shebang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t need shebang __init__.py files. --- Lib/fontTools/mtiLib/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/fontTools/mtiLib/__init__.py b/Lib/fontTools/mtiLib/__init__.py index 8896f24463..e797be375b 100644 --- a/Lib/fontTools/mtiLib/__init__.py +++ b/Lib/fontTools/mtiLib/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # FontDame-to-FontTools for OpenType Layout tables # # Source language spec is available at: From f7187371fa840601e69dea0c0ac1352959d2f4ac Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 19 Apr 2025 21:28:53 +0200 Subject: [PATCH 046/105] [feaLib] Upgrade single substitutions mixed with ligature ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a lookup has only single and ligature substitutions, we now upgrade single substitutions to ligature ones. Previously this would cause an error in named lookup, and would result in splitting lookups in anonymous ones. This is similar to what we have done with multiple substitutions in https://github.com/fonttools/fonttools/pull/1133, but back then I incorrectly convinced myself that ligature substitutions can’t have a single source glyph, which is obviously not true. --- Lib/fontTools/feaLib/parser.py | 60 +++++++++++++++++++++------- Tests/feaLib/builder_test.py | 71 ++++++++++++++++++++++++++++++++++ Tests/feaLib/data/spec8a.ttx | 51 ++++++++++-------------- Tests/feaLib/parser_test.py | 24 ------------ 4 files changed, 137 insertions(+), 69 deletions(-) diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 5f647ca0ac..0b3e6d7feb 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -1613,7 +1613,7 @@ def parse_base_script_list_(self, count): "HorizAxis.BaseScriptList", "VertAxis.BaseScriptList", ), self.cur_token_ - scripts = [(self.parse_base_script_record_(count))] + scripts = [self.parse_base_script_record_(count)] while self.next_token_ == ",": self.expect_symbol_(",") scripts.append(self.parse_base_script_record_(count)) @@ -2065,19 +2065,37 @@ def parse_block_( # 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. + # 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. - # Check if we have a mix of non-contextual singles and multiples. + # Check if we have a mix of non-contextual singles and multiples or + # ligatures. has_single = False has_multiple = False + has_ligature = 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]) + elif isinstance(s, self.ast.LigatureSubstStatement): + has_ligature = not any([s.prefix, s.suffix, s.forceChain]) - # Upgrade all single substitutions to multiple substitutions. - if has_single and has_multiple: + 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: statements = [] for s in block.statements: if isinstance(s, self.ast.SingleSubstStatement): @@ -2085,17 +2103,29 @@ def parse_block_( 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, + for glyph, replacement in zip(glyphs, replacements): + if to_multiple: + statements.append( + self.ast.MultipleSubstStatement( + s.prefix, + glyph, + s.suffix, + [replacement], + s.forceChain, + location=s.location, + ) + ) + elif to_ligature: + statements.append( + self.ast.LigatureSubstStatement( + s.prefix, + [self.ast.GlyphName(glyph)], + s.suffix, + replacement, + s.forceChain, + location=s.location, + ) ) - ) else: statements.append(s) block.statements = statements diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 5b8aa798f7..97ee9d7b2a 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -91,6 +91,10 @@ class BuilderTest(unittest.TestCase): MarkBasePosSubtable """.split() + FEA2FEA_IGNORE = """ + spec8a + """.split() + VARFONT_AXES = [ ("wght", 200, 200, 1000, "Weight"), ("wdth", 100, 100, 200, "Width"), @@ -301,6 +305,71 @@ def test_multipleSubst_multipleIdenticalSubstitutionsForSameGlyph_info(self): r"Removing duplicate multiple substitution from glyph \"f_f_i\" to \('f', 'f', 'i'\)" ) + def test_mixed_singleSubst_multipleSubst(self): + font = self.build( + "lookup test {" + " sub f_f by f f;" + " sub f by f;" + " sub f_f_i by f f i;" + " sub [A A.sc] by A;" + " sub [B B.sc] by [B B.sc];" + "} test;" + ) + + assert "GSUB" in font + st = font["GSUB"].table.LookupList.Lookup[0].SubTable[0] + self.assertEqual(st.LookupType, 2) + self.assertEqual( + st.mapping, + { + "f_f": ("f", "f"), + "f": ("f",), + "f_f_i": ("f", "f", "i"), + "A": ("A",), + "A.sc": ("A",), + "B": ("B",), + "B.sc": ("B.sc",), + }, + ) + + def test_mixed_singleSubst_ligatureSubst(self): + font = self.build( + "lookup test {" + " sub f f by f_f;" + " sub f f i by f_f_i;" + " sub A by A.sc;" + "} test;" + ) + + assert "GSUB" in font + st = font["GSUB"].table.LookupList.Lookup[0].SubTable[0] + self.assertEqual(st.LookupType, 4) + self.assertEqual(len(st.ligatures), 2) + self.assertEqual(len(st.ligatures["f"]), 2) + self.assertEqual(st.ligatures["f"][0].LigGlyph, "f_f_i") + self.assertEqual(len(st.ligatures["f"][0].Component), 2) + self.assertEqual(st.ligatures["f"][0].Component[0], "f") + self.assertEqual(st.ligatures["f"][0].Component[1], "i") + self.assertEqual(st.ligatures["f"][1].LigGlyph, "f_f") + self.assertEqual(len(st.ligatures["f"][1].Component), 1) + self.assertEqual(st.ligatures["f"][1].Component[0], "f") + self.assertEqual(len(st.ligatures["A"]), 1) + self.assertEqual(st.ligatures["A"][0].LigGlyph, "A.sc") + self.assertEqual(len(st.ligatures["A"][0].Component), 0) + + def test_mixed_singleSubst_multipleSubst_ligatureSubst(self): + self.assertRaisesRegex( + FeatureLibError, + "Within a named lookup block, all rules must be of the " + "same lookup type and flag", + self.build, + "lookup test {" + " sub A by A.sc;" + " sub f_f by f f;" + " sub f f i by f_f_i;" + "} test;", + ) + def test_pairPos_redefinition_warning(self): # https://github.com/fonttools/fonttools/issues/1147 logger = logging.getLogger("fontTools.otlLib.builder") @@ -1195,6 +1264,8 @@ def generate_fea2fea_file_test(name): for name in BuilderTest.TEST_FEATURE_FILES: + if name in BuilderTest.FEA2FEA_IGNORE: + continue setattr( BuilderTest, "test_Fea2feaFile_{}".format(name), 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/parser_test.py b/Tests/feaLib/parser_test.py index bee00d9d71..23124536e5 100644 --- a/Tests/feaLib/parser_test.py +++ b/Tests/feaLib/parser_test.py @@ -1717,30 +1717,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;" From a1136d6cc7510080433680911b7ba36036f989b7 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 01:33:33 +0200 Subject: [PATCH 047/105] [feaLib] Move upgrading single subst statements to ast Doing this is Block.build() is less of a hack than modifying the parsed ast tree during parsing. --- Lib/fontTools/feaLib/ast.py | 73 +++++++++++++++++++++++++++++++++- Lib/fontTools/feaLib/parser.py | 68 ------------------------------- Tests/feaLib/builder_test.py | 6 --- 3 files changed, 72 insertions(+), 75 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index eeb09e736a..5325964c71 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=""): diff --git a/Lib/fontTools/feaLib/parser.py b/Lib/fontTools/feaLib/parser.py index 0b3e6d7feb..451dd62411 100644 --- a/Lib/fontTools/feaLib/parser.py +++ b/Lib/fontTools/feaLib/parser.py @@ -2062,74 +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. 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. - - # Check if we have a mix of non-contextual singles and multiples or - # ligatures. - has_single = False - has_multiple = False - has_ligature = 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]) - elif isinstance(s, self.ast.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: - 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 glyph, replacement in zip(glyphs, replacements): - if to_multiple: - statements.append( - self.ast.MultipleSubstStatement( - s.prefix, - glyph, - s.suffix, - [replacement], - s.forceChain, - location=s.location, - ) - ) - elif to_ligature: - statements.append( - self.ast.LigatureSubstStatement( - s.prefix, - [self.ast.GlyphName(glyph)], - s.suffix, - replacement, - 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/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 97ee9d7b2a..a2435cde2d 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -91,10 +91,6 @@ class BuilderTest(unittest.TestCase): MarkBasePosSubtable """.split() - FEA2FEA_IGNORE = """ - spec8a - """.split() - VARFONT_AXES = [ ("wght", 200, 200, 1000, "Weight"), ("wdth", 100, 100, 200, "Width"), @@ -1264,8 +1260,6 @@ def generate_fea2fea_file_test(name): for name in BuilderTest.TEST_FEATURE_FILES: - if name in BuilderTest.FEA2FEA_IGNORE: - continue setattr( BuilderTest, "test_Fea2feaFile_{}".format(name), From e40a8c446cb73dee236f8545c291b47655373d34 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 23:39:04 +0200 Subject: [PATCH 048/105] [feaLib] Support subtable statements in MarkLigPos lookups --- Lib/fontTools/otlLib/builder.py | 44 +++-- Tests/feaLib/builder_test.py | 1 + Tests/feaLib/data/MarkLigPosSubtable.fea | 25 +++ Tests/feaLib/data/MarkLigPosSubtable.ttx | 214 +++++++++++++++++++++++ 4 files changed, 270 insertions(+), 14 deletions(-) create mode 100644 Tests/feaLib/data/MarkLigPosSubtable.fea create mode 100644 Tests/feaLib/data/MarkLigPosSubtable.ttx diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 4bd5f4d112..255edbfe12 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1088,17 +1088,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): @@ -1108,18 +1116,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. diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 5b8aa798f7..93c5880b80 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -89,6 +89,7 @@ class BuilderTest(unittest.TestCase): duplicate_language_stmt CursivePosSubtable MarkBasePosSubtable + MarkLigPosSubtable """.split() VARFONT_AXES = [ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 861c41a480151737ea0ab5f5261606c252bda67f Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 23:48:58 +0200 Subject: [PATCH 049/105] [otlLib] Deprecate buildMarkLigPos() It is now unused and it never did anything other than calling buildMarkLigPosSubtable(). --- Lib/fontTools/otlLib/builder.py | 48 ++++++++++----------------------- 1 file changed, 14 insertions(+), 34 deletions(-) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 255edbfe12..69e971a05e 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -19,6 +19,7 @@ compact_lookup, ) from fontTools.otlLib.error import OpenTypeLibError +from fontTools.misc.loggingTools import deprecateFunction from functools import reduce import logging import copy @@ -1982,14 +1983,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 @@ -2010,37 +2018,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 From 19d728762a6e651f16182cfff2e86fe4e798c24d Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 23:51:54 +0200 Subject: [PATCH 050/105] [otlLib] Restore and deprecated buildMarkBasePos() --- Lib/fontTools/otlLib/builder.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 69e971a05e..fa7efb5a69 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1944,6 +1944,18 @@ def buildMarkArray(marks, glyphMap): return self +@deprecateFunction( + "use buildMarkBasePosSubtable() instead", category=DeprecationWarning +) +def buildMarkBasePos(marks, bases, glyphMap): + """Build a list of MarkBasePos (GPOS4) subtables. + + .. deprecated:: 4.58.0 + Use :func:`buildMarkBasePosSubtable` instead. + """ + return [buildMarkBasePosSubtable(marks, bases, glyphMap)] + + def buildMarkBasePosSubtable(marks, bases, glyphMap): """Build a single MarkBasePos (GPOS4) subtable. From 60864108ca90f9b3ef3f5456a10307ef563645de Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 21 Apr 2025 00:07:36 +0200 Subject: [PATCH 051/105] [feaLib] Support subtable statements in MarkMarkPos lookups --- Lib/fontTools/otlLib/builder.py | 61 +++++--- Tests/feaLib/builder_test.py | 1 + Tests/feaLib/data/MarkMarkPosSubtable.fea | 18 +++ Tests/feaLib/data/MarkMarkPosSubtable.ttx | 170 ++++++++++++++++++++++ 4 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 Tests/feaLib/data/MarkMarkPosSubtable.fea create mode 100644 Tests/feaLib/data/MarkMarkPosSubtable.ttx diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index fa7efb5a69..908672d749 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1169,17 +1169,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): @@ -1189,25 +1197,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(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]) + 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) + + def add_subtable_break(self, location): + self.subtables_.append((self.marks, self.baseMarks)) + self.marks = {} + self.baseMarks = {} class ReverseChainSingleSubstBuilder(LookupBuilder): diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 93c5880b80..23034ea293 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -90,6 +90,7 @@ class BuilderTest(unittest.TestCase): CursivePosSubtable MarkBasePosSubtable MarkLigPosSubtable + MarkMarkPosSubtable """.split() VARFONT_AXES = [ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 16a610bb332d61de498d9713947ff077b427ec10 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 21 Apr 2025 02:26:03 +0200 Subject: [PATCH 052/105] [ttLib] Fix `AttributeError` when reporting table overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `OTTableWriter.getOverflowErrorRecord()` method accesses `name` attribute unconditionally, but `OTTableWriter()` constructor does not set it and instead call sites like in otConvertor.py set the name. But not all call sites set the name. In my case an overflow is happening `otlLib.builder.ChainContextualBuilder.getCompiledSize_()` which constructs an `OTTableWriter` and uses it without setting a name. I don’t even know what name it should use, and it feels less error prune for `OTTableWriter` constructor to set an initial name. --- Lib/fontTools/ttLib/tables/otBase.py | 1 + 1 file changed, 1 insertion(+) 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() From 3d56bbbed6e1ef3125ae942e3aa025250ab4fd7a Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 22 Apr 2025 05:03:14 +0200 Subject: [PATCH 053/105] [post] Change the separator for duplicate names from "#" to "." Using "#" in glyph names can be problematic, e.g. it is not valid if feature files ("#" starts a comment). This commit uses the less problematic "." instead. Because dot is a common glyph name separator, if the font has ["A", "A", "A.1"] names, we will rename the second glyph to A.1, and then the third glyph will be seen as duplicate and gets renamed to A.1.1, which is probably undesired. The code have been slightly modified to make sure the new glyph name does not exist in the font as whole, not only the already seen glyphs. --- Lib/fontTools/ttLib/tables/_p_o_s_t.py | 7 +++++-- Tests/ttLib/tables/_p_o_s_t_test.py | 16 ++++++++++++++++ .../ttLib/tables/data/duplicate_glyph_name.ttf | Bin 0 -> 628 bytes 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 Tests/ttLib/tables/_p_o_s_t_test.py create mode 100644 Tests/ttLib/tables/data/duplicate_glyph_name.ttf diff --git a/Lib/fontTools/ttLib/tables/_p_o_s_t.py b/Lib/fontTools/ttLib/tables/_p_o_s_t.py index fca0812f98..c449e5f0c0 100644 --- a/Lib/fontTools/ttLib/tables/_p_o_s_t.py +++ b/Lib/fontTools/ttLib/tables/_p_o_s_t.py @@ -122,13 +122,16 @@ def build_psNameMapping(self, ttFont): glyphName = psName = self.glyphOrder[i] if glyphName == "": glyphName = "glyph%.5d" % i + if glyphName in allNames: # make up a new glyphName that's unique n = allNames[glyphName] - while (glyphName + "#" + str(n)) in allNames: + # check if the exists in any of the seen names or later ones + names = set(allNames.keys()) | set(self.glyphOrder) + while (glyphName + "." + str(n)) in names: n += 1 allNames[glyphName] = n + 1 - glyphName = glyphName + "#" + str(n) + glyphName = glyphName + "." + str(n) self.glyphOrder[i] = glyphName allNames[glyphName] = 1 diff --git a/Tests/ttLib/tables/_p_o_s_t_test.py b/Tests/ttLib/tables/_p_o_s_t_test.py new file mode 100644 index 0000000000..509003efc2 --- /dev/null +++ b/Tests/ttLib/tables/_p_o_s_t_test.py @@ -0,0 +1,16 @@ +from fontTools.ttLib import TTFont +import os + + +CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +DATA_DIR = os.path.join(CURR_DIR, "data") + + +def test_duplicate_glyph_names(): + font_path = os.path.join(DATA_DIR, "duplicate_glyph_name.ttf") + font = TTFont(font_path) + + assert font.getGlyphOrder() == [".notdef", "space", "A", "A.2", "A.1"] + + post = font["post"] + assert post.mapping == {"A.2": "A"} diff --git a/Tests/ttLib/tables/data/duplicate_glyph_name.ttf b/Tests/ttLib/tables/data/duplicate_glyph_name.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f7584c685b7561e1fda653d1750c628771cb6498 GIT binary patch literal 628 zcmZuuyGjE=6g{)ENqk}A55z4N4B{IRixdXLCJ;gKQK_ruQDl=v6O5gOh+0`#i1-N> zVr`*a3O_(l1R+H#D{XAnGrL(8^}@`#=iEDY&&)yq=)fK%jM@3&vHa`$0U#1o*EVd+ zK?O_O$#+QEq=Lr4 z)s%;WRRuvrptVw-kxh3Jb#3`u3EDiA-t5qqCU`cE2%ra&E*`}RLTJ;Dy(wuQ#R|so z5j3?>{^Sekr3`LiASDgufu8HEtGhtFX*UFeH&o83e>qegzN;>pNU$0vDqo!By51?G z`{LgDv#9(HiGDRjZ)Z=8Wk1)@P3Gre-ZPF2ve@C|8I)1tKc6GdqlzLdY_+OEZSYr> Xru`4Q7krEVLcObm2~%h?H8lDOJNQzB literal 0 HcmV?d00001 From 0698c5495a6065ab3ebba8547a3e336c59bf245a Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 22 Apr 2025 19:08:25 +0200 Subject: [PATCH 054/105] [cffLib] Change the separator for duplicate names from "#" to "." Similar to the change to post table. --- Lib/fontTools/cffLib/__init__.py | 5 +++-- Tests/ttLib/data/duplicate_glyph_name.otf | Bin 0 -> 1160 bytes .../data/duplicate_glyph_name.ttf | Bin Tests/ttLib/tables/_p_o_s_t_test.py | 16 ---------------- Tests/ttLib/ttFont_test.py | 18 ++++++++++++++++++ 5 files changed, 21 insertions(+), 18 deletions(-) create mode 100644 Tests/ttLib/data/duplicate_glyph_name.otf rename Tests/ttLib/{tables => }/data/duplicate_glyph_name.ttf (100%) delete mode 100644 Tests/ttLib/tables/_p_o_s_t_test.py diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index d75e23b750..8ebad975fb 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1464,10 +1464,11 @@ def _read(self, parent, value): if glyphName in allNames: # make up a new glyphName that's unique n = allNames[glyphName] - while (glyphName + "#" + str(n)) in allNames: + names = set(allNames) | set(charset) + while (glyphName + "." + str(n)) in names: n += 1 allNames[glyphName] = n + 1 - glyphName = glyphName + "#" + str(n) + glyphName = glyphName + "." + str(n) allNames[glyphName] = 1 newCharset.append(glyphName) charset = newCharset diff --git a/Tests/ttLib/data/duplicate_glyph_name.otf b/Tests/ttLib/data/duplicate_glyph_name.otf new file mode 100644 index 0000000000000000000000000000000000000000..66808c0179b0b14435116cd23baf0a4542fa8e61 GIT binary patch literal 1160 zcmd5*OKTHR6#nkaooT8iO=~MI#xNA63MMu|MMS}%WMLqYTD1$&jyfhn^B@lz(1km} zY>IYSaPLao2`&^|^ar?eB^1m;T&WDY7{5C+@lo2i^}w0)`p)B?xwo{iP{Jr4Ku6+S zK5yUKgGE5!1n{JE>1ek6`Q|l1(8SWMs?$J6e+g(^`Yu#l=k{#(&Oyf3$tNmw$QNRb z`aAN0O10fJhyidAYc?NNX( zgi>7R9N+)SVbMSB%Y~~&yylcS-B!Ig+Xmz8&CeI@6(ivZOca?ZdUmJqzA`%}eUuwEQtKruRsOh1R&mH*4YkgoT)e-Xx@gON{VVCUfabCOx;M pAWd5}P%{YL=o#@4h7g)G%#rxFILZ4o)t~N-?LzR@_+ADt^d~%di&Ovr literal 0 HcmV?d00001 diff --git a/Tests/ttLib/tables/data/duplicate_glyph_name.ttf b/Tests/ttLib/data/duplicate_glyph_name.ttf similarity index 100% rename from Tests/ttLib/tables/data/duplicate_glyph_name.ttf rename to Tests/ttLib/data/duplicate_glyph_name.ttf diff --git a/Tests/ttLib/tables/_p_o_s_t_test.py b/Tests/ttLib/tables/_p_o_s_t_test.py deleted file mode 100644 index 509003efc2..0000000000 --- a/Tests/ttLib/tables/_p_o_s_t_test.py +++ /dev/null @@ -1,16 +0,0 @@ -from fontTools.ttLib import TTFont -import os - - -CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) -DATA_DIR = os.path.join(CURR_DIR, "data") - - -def test_duplicate_glyph_names(): - font_path = os.path.join(DATA_DIR, "duplicate_glyph_name.ttf") - font = TTFont(font_path) - - assert font.getGlyphOrder() == [".notdef", "space", "A", "A.2", "A.1"] - - post = font["post"] - assert post.mapping == {"A.2": "A"} 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"} From aff8f6672e2fa64b705462c6092cdaef8f02ab8d Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 17 Apr 2025 15:53:19 +0200 Subject: [PATCH 055/105] [feaLib] Fix serializing SinglePos with empty ValueRecord bool(ValueRecord()) evaluates to False, which means "pos foo ;" gets serialized to "pos foo ;" which is invalid. --- Lib/fontTools/feaLib/ast.py | 9 +++++++-- Tests/feaLib/ast_test.py | 22 ++++++++++++++++++++++ Tests/feaLib/parser_test.py | 10 ++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index 5325964c71..ac610430ce 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -1583,7 +1583,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 ] ) @@ -1591,7 +1593,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 diff --git a/Tests/feaLib/ast_test.py b/Tests/feaLib/ast_test.py index ebae3dab27..4701e01011 100644 --- a/Tests/feaLib/ast_test.py +++ b/Tests/feaLib/ast_test.py @@ -13,12 +13,34 @@ def test_valuerecord_none(self): statement = ast.ValueRecord(xPlacement=10, xAdvance=20) self.assertEqual(statement.asFea(), "<10 0 20 0>") + def test_valuerecord_empty(self): + statement = ast.ValueRecord() + self.assertEqual(statement.asFea(), "") + def test_non_object_location(self): el = ast.Element(location=("file.fea", 1, 2)) self.assertEqual(el.location.file, "file.fea") self.assertEqual(el.location.line, 1) self.assertEqual(el.location.column, 2) + def test_single_pos_statement_empty_valuerecord(self): + statement = ast.SinglePosStatement( + pos=[(ast.GlyphName("a"), ast.ValueRecord())], + prefix=[], + suffix=[], + forceChain=False, + ) + self.assertEqual(statement.asFea(), "pos a ;") + + def test_single_pos_statement_empty_valuerecord_chain(self): + statement = ast.SinglePosStatement( + pos=[(ast.GlyphName("a"), ast.ValueRecord())], + prefix=[], + suffix=[], + forceChain=True, + ) + self.assertEqual(statement.asFea(), "pos a' ;") + if __name__ == "__main__": import sys diff --git a/Tests/feaLib/parser_test.py b/Tests/feaLib/parser_test.py index 23124536e5..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;" From a64468ae79b2c48f177becc29972bf1f2323fc4d Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 17 Apr 2025 17:26:59 +0200 Subject: [PATCH 056/105] [feaLib] Correctly handle in single pos lookups This was causing an AssertionError: File "/fonttools/Lib/fontTools/otlLib/builder.py", line 1528, in can_add assert isinstance(value, ValueRecord) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError --- Lib/fontTools/otlLib/builder.py | 2 + Tests/feaLib/builder_test.py | 1 + Tests/feaLib/data/single_pos_NULL.fea | 8 ++++ Tests/feaLib/data/single_pos_NULL.ttx | 57 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+) create mode 100644 Tests/feaLib/data/single_pos_NULL.fea create mode 100644 Tests/feaLib/data/single_pos_NULL.ttx diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 908672d749..60d6d368ef 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1545,6 +1545,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( diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 4a6a304887..35dd1f1032 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -91,6 +91,7 @@ class BuilderTest(unittest.TestCase): MarkBasePosSubtable MarkLigPosSubtable MarkMarkPosSubtable + single_pos_NULL """.split() VARFONT_AXES = [ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 95b5747853849440ee8819292a92cbba5556978b Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 17 Apr 2025 17:58:48 +0200 Subject: [PATCH 057/105] [feaLib] Remove duplicates from class pair pos classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AFDKO’s makeotf removes the duplicates and produces the following message: NOTE: [line 3 char 29] Removing duplicate glyph NOTE: [line 3 char 29] Removing duplicate glyph While feaLib fails with: Traceback (most recent call last): File "/fonttools/Lib/fontTools/feaLib/builder.py", line 875, in buildLookups_ otLookups.append(l.build()) ^^^^^^^^^ File "/fonttools/Lib/fontTools/otlLib/builder.py", line 1460, in build builder.addPair(glyphclass1, value1, glyphclass2, value2) File "/fonttools/Lib/fontTools/otlLib/builder.py", line 1340, in addPair self.classDef1_.add(gc1) File "/fonttools/Lib/fontTools/otlLib/builder.py", line 2656, in add raise OpenTypeLibError( fontTools.otlLib.error.OpenTypeLibError: Glyph A is already present in class. --- Lib/fontTools/feaLib/builder.py | 4 +- Tests/feaLib/builder_test.py | 1 + .../feaLib/data/class_pair_pos_duplicates.fea | 4 ++ .../feaLib/data/class_pair_pos_duplicates.ttx | 66 +++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 Tests/feaLib/data/class_pair_pos_duplicates.fea create mode 100644 Tests/feaLib/data/class_pair_pos_duplicates.ttx diff --git a/Lib/fontTools/feaLib/builder.py b/Lib/fontTools/feaLib/builder.py index 8b2c7208b2..4603606982 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -1471,7 +1471,9 @@ def add_class_pair_pos(self, location, glyphclass1, value1, glyphclass2, value2) lookup = self.get_lookup_(location, PairPosBuilder) v1 = self.makeOpenTypeValueRecord(location, value1, pairPosContext=True) v2 = self.makeOpenTypeValueRecord(location, value2, pairPosContext=True) - lookup.addClassPair(location, glyphclass1, v1, glyphclass2, v2) + cls1 = tuple(sorted(set(glyphclass1))) + cls2 = tuple(sorted(set(glyphclass2))) + lookup.addClassPair(location, cls1, v1, cls2, v2) def add_specific_pair_pos(self, location, glyph1, value1, glyph2, value2): if not glyph1 or not glyph2: diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 35dd1f1032..1189d5f5b6 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -92,6 +92,7 @@ class BuilderTest(unittest.TestCase): MarkLigPosSubtable MarkMarkPosSubtable single_pos_NULL + class_pair_pos_duplicates """.split() VARFONT_AXES = [ 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a74cb210d2f176d701d34f1c3ac7f77663522691 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 24 Apr 2025 03:08:40 +0200 Subject: [PATCH 058/105] [feaLib] Support creating extension lookups Respect useExtension keyword instead of silently ignoring it. --- Lib/fontTools/feaLib/ast.py | 8 +- Lib/fontTools/feaLib/builder.py | 20 ++- Lib/fontTools/otlLib/builder.py | 31 ++++- Tests/feaLib/builder_test.py | 11 +- Tests/feaLib/data/spec8a_2.fea | 21 +++ Tests/feaLib/data/spec8a_2.ttx | 197 +++++++++++++++++++++++++++++ Tests/feaLib/data/useExtension.fea | 32 +++++ Tests/feaLib/data/useExtension.ttx | 178 ++++++++++++++++++++++++++ 8 files changed, 485 insertions(+), 13 deletions(-) create mode 100644 Tests/feaLib/data/spec8a_2.fea create mode 100644 Tests/feaLib/data/spec8a_2.ttx create mode 100644 Tests/feaLib/data/useExtension.fea create mode 100644 Tests/feaLib/data/useExtension.ttx diff --git a/Lib/fontTools/feaLib/ast.py b/Lib/fontTools/feaLib/ast.py index ac610430ce..8479d7300d 100644 --- a/Lib/fontTools/feaLib/ast.py +++ b/Lib/fontTools/feaLib/ast.py @@ -453,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_ @@ -504,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() @@ -2179,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 4603606982..1583f06d9e 100644 --- a/Lib/fontTools/feaLib/builder.py +++ b/Lib/fontTools/feaLib/builder.py @@ -126,6 +126,7 @@ def __init__(self, font, featurefile): self.script_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None + self.use_extension_ = False self.language_systems = set() self.seen_non_DFLT_script_ = False self.named_lookups_ = {} @@ -141,6 +142,7 @@ def __init__(self, font, featurefile): self.aalt_features_ = [] # [(location, featureName)*], for 'aalt' self.aalt_location_ = None self.aalt_alternates_ = {} + self.aalt_use_extension_ = False # for 'featureNames' self.featureNames_ = set() self.featureNames_ids_ = {} @@ -247,6 +249,7 @@ def get_chained_lookup_(self, location, builder_class): result = builder_class(self.font, location) result.lookupflag = self.lookupflag_ result.markFilterSet = self.lookupflag_markFilterSet_ + result.extension = self.use_extension_ self.lookups_.append(result) return result @@ -272,6 +275,7 @@ def get_lookup_(self, location, builder_class): self.cur_lookup_ = builder_class(self.font, location) self.cur_lookup_.lookupflag = self.lookupflag_ self.cur_lookup_.markFilterSet = self.lookupflag_markFilterSet_ + self.cur_lookup_.extension = self.use_extension_ self.lookups_.append(self.cur_lookup_) if self.cur_lookup_name_: # We are starting a lookup rule inside a named lookup block. @@ -323,7 +327,7 @@ def build_feature_aalt_(self): } old_lookups = self.lookups_ self.lookups_ = [] - self.start_feature(self.aalt_location_, "aalt") + self.start_feature(self.aalt_location_, "aalt", self.aalt_use_extension_) if single: single_lookup = self.get_lookup_(location, SingleSubstBuilder) single_lookup.mapping = single @@ -1054,15 +1058,22 @@ def get_default_language_systems_(self): else: return frozenset({("DFLT", "dflt")}) - def start_feature(self, location, name): + def start_feature(self, location, name, use_extension=False): + if use_extension and name != "aalt": + raise FeatureLibError( + "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature", + location, + ) self.language_systems = self.get_default_language_systems_() self.script_ = "DFLT" self.cur_lookup_ = None self.cur_feature_name_ = name self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None + self.use_extension_ = use_extension if name == "aalt": self.aalt_location_ = location + self.aalt_use_extension_ = use_extension def end_feature(self): assert self.cur_feature_name_ is not None @@ -1071,8 +1082,9 @@ def end_feature(self): self.cur_lookup_ = None self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None + self.use_extension_ = False - def start_lookup_block(self, location, name): + def start_lookup_block(self, location, name, use_extension=False): if name in self.named_lookups_: raise FeatureLibError( 'Lookup "%s" has already been defined' % name, location @@ -1086,6 +1098,7 @@ def start_lookup_block(self, location, name): self.cur_lookup_name_ = name self.named_lookups_[name] = None self.cur_lookup_ = None + self.use_extension_ = use_extension if self.cur_feature_name_ is None: self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None @@ -1094,6 +1107,7 @@ def end_lookup_block(self): assert self.cur_lookup_name_ is not None self.cur_lookup_name_ = None self.cur_lookup_ = None + self.use_extension_ = False if self.cur_feature_name_ is None: self.lookupflag_ = 0 self.lookupflag_markFilterSet_ = None diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 60d6d368ef..324e36b8a5 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -74,7 +74,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 `__) @@ -99,6 +99,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 @@ -114,8 +116,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) @@ -134,7 +149,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 @@ -142,6 +157,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): @@ -150,6 +166,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): @@ -161,7 +178,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} diff --git a/Tests/feaLib/builder_test.py b/Tests/feaLib/builder_test.py index 1189d5f5b6..aa869806fd 100644 --- a/Tests/feaLib/builder_test.py +++ b/Tests/feaLib/builder_test.py @@ -68,7 +68,7 @@ class BuilderTest(unittest.TestCase): spec4h1 spec4h2 spec5d1 spec5d2 spec5fi1 spec5fi2 spec5fi3 spec5fi4 spec5f_ii_1 spec5f_ii_2 spec5f_ii_3 spec5f_ii_4 spec5h1 spec6b_ii spec6d2 spec6e spec6f - spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8b spec8c spec8d + spec6h_ii spec6h_iii_1 spec6h_iii_3d spec8a spec8a_2 spec8b spec8c spec8d spec9a spec9a2 spec9b spec9c1 spec9c2 spec9c3 spec9d spec9e spec9f spec9g spec10 bug453 bug457 bug463 bug501 bug502 bug504 bug505 bug506 bug509 @@ -93,6 +93,7 @@ class BuilderTest(unittest.TestCase): MarkMarkPosSubtable single_pos_NULL class_pair_pos_duplicates + useExtension """.split() VARFONT_AXES = [ @@ -1250,6 +1251,14 @@ def test_variable_anchors_round_trip(self): addOpenTypeFeatures(font, feafile) assert dedent(str(feafile)) == dedent(features) + def test_feature_useExtension(self): + self.assertRaisesRegex( + FeatureLibError, + "'useExtension' keyword for feature blocks is allowed only for 'aalt' feature", + self.build, + "feature liga useExtension { sub f f by f_f; } liga;", + ) + def generate_feature_file_test(name): return lambda self: self.check_feature_file(name) diff --git a/Tests/feaLib/data/spec8a_2.fea b/Tests/feaLib/data/spec8a_2.fea new file mode 100644 index 0000000000..68f558a45c --- /dev/null +++ b/Tests/feaLib/data/spec8a_2.fea @@ -0,0 +1,21 @@ +languagesystem DFLT dflt; +languagesystem latn dflt; +languagesystem latn TRK; +languagesystem cyrl dflt; + +feature aalt useExtension { + feature salt; + feature smcp; + sub d by d.alt; +} aalt; + +feature smcp { + sub [a - c] by [A.sc - C.sc]; + sub f i by f_i; # not considered for aalt +} smcp; + +feature salt { + sub a from [a.alt1 a.alt2 a.alt3]; + sub e [c d e]' f by [c.mid d.mid e.mid]; + sub b by b.alt; +} salt; diff --git a/Tests/feaLib/data/spec8a_2.ttx b/Tests/feaLib/data/spec8a_2.ttx new file mode 100644 index 0000000000..5cb4a4a410 --- /dev/null +++ b/Tests/feaLib/data/spec8a_2.ttx @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/feaLib/data/useExtension.fea b/Tests/feaLib/data/useExtension.fea new file mode 100644 index 0000000000..d2eef02280 --- /dev/null +++ b/Tests/feaLib/data/useExtension.fea @@ -0,0 +1,32 @@ +languagesystem DFLT dflt; + +lookup liga1 useExtension { + sub f i by f_i; +} liga1; + +lookup liga2 { + sub f l by f_l; +} liga2; + +lookup liga3 useExtension { + sub a f' f' i' by f_f_i; +} liga3; + +feature liga { + lookup liga1; + lookup liga2; + lookup liga3; +} liga; + +lookup kern1 useExtension { + pos A V -100; +} kern1; + +lookup kern2 { + pos V A -100; +} kern2; + +feature kern { + lookup kern1; + lookup kern2; +} kern; \ No newline at end of file diff --git a/Tests/feaLib/data/useExtension.ttx b/Tests/feaLib/data/useExtension.ttx new file mode 100644 index 0000000000..2dedede70a --- /dev/null +++ b/Tests/feaLib/data/useExtension.ttx @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From b0cdc1ded392ae2df99c45e5e5df223568e495e1 Mon Sep 17 00:00:00 2001 From: Harry Dalton Date: Fri, 25 Apr 2025 12:27:55 +0100 Subject: [PATCH 059/105] Add typing for the simpler STAT arguments As well as general creature comforts, this avoids popular Python type checkers assuming that the elided fallback name argument can only accept an integer. --- Lib/fontTools/otlLib/builder.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 324e36b8a5..d296245f60 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1,6 +1,7 @@ from collections import namedtuple, OrderedDict import itertools import os +from typing import Dict, Union from fontTools.misc.fixedTools import fixedToFloat from fontTools.misc.roundTools import otRound from fontTools import ttLib @@ -13,6 +14,7 @@ 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, @@ -2741,10 +2743,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 @@ -2935,7 +2945,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 From 63e600059400fc7d60bdc63c496c20c15ee69759 Mon Sep 17 00:00:00 2001 From: Harry Dalton Date: Fri, 25 Apr 2025 12:33:44 +0100 Subject: [PATCH 060/105] Prune unused imports --- Lib/fontTools/otlLib/builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index d296245f60..1c3222efeb 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1,6 +1,5 @@ from collections import namedtuple, OrderedDict import itertools -import os from typing import Dict, Union from fontTools.misc.fixedTools import fixedToFloat from fontTools.misc.roundTools import otRound @@ -11,9 +10,7 @@ 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 ( From aaac0862fbc581b4863bbf94fb72bb7033987bc2 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 11:47:53 +0100 Subject: [PATCH 061/105] Add future import for annotations We don't need to evaluate types here. --- Lib/fontTools/otlLib/builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index 1c3222efeb..064b2fce31 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import namedtuple, OrderedDict import itertools from typing import Dict, Union From 8242c31ac33c06c11729b4c7baba08b44a3d7e4f Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 19 Apr 2025 07:37:51 +0300 Subject: [PATCH 062/105] fix logic error in parseEncoding1 - range-based encoding if two ranges existed one after another it would fail. --- Lib/fontTools/cffLib/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 8ebad975fb..9774f309a3 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1734,13 +1734,13 @@ def parseEncoding1(charset, file, haveSupplement, strings): 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 From 1394c643be69be99d5bd57d6ff59b0b878e8b9d6 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 19 Apr 2025 07:38:57 +0300 Subject: [PATCH 063/105] fix read supplement encoding --- Lib/fontTools/cffLib/__init__.py | 76 ++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 9774f309a3..854d26172c 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1664,25 +1664,27 @@ 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, haveSupplement, parent.strings + ) + elif fmt == 1: + encoding = parseEncoding1( + parent.charset, file, haveSupplement, parent.strings + ) 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}") + + return encoding def write(self, parent, value): if value == "StandardEncoding": @@ -1720,17 +1722,51 @@ def xmlRead(self, name, attrs, content, parent): return encoding +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, haveSupplement, strings): + """ + 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] + + if haveSupplement: + parseEncodingSupplement(file, encoding, strings) + return encoding def parseEncoding1(charset, file, haveSupplement, strings): + """ + Format 1: range-based encoding. + After reading the base ranges, optionally parse the supplement. + """ nRanges = readCard8(file) encoding = [".notdef"] * 256 glyphID = 1 @@ -1741,6 +1777,10 @@ def parseEncoding1(charset, file, haveSupplement, strings): encoding[code] = charset[glyphID] code += 1 glyphID += 1 + + if haveSupplement: + parseEncodingSupplement(file, encoding, strings) + return encoding From 28d9814ea98101e775caa0946559fe1656bee595 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Sat, 19 Apr 2025 09:04:28 +0300 Subject: [PATCH 064/105] fix read supplement encoding - refactor --- Lib/fontTools/cffLib/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index 854d26172c..cf18f914b4 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1675,15 +1675,18 @@ def _read(self, parent, value): if fmt == 0: encoding = parseEncoding0( - parent.charset, file, haveSupplement, parent.strings + parent.charset, file ) elif fmt == 1: encoding = parseEncoding1( - parent.charset, file, haveSupplement, parent.strings + parent.charset, file ) else: raise ValueError(f"Unknown Encoding format: {fmt}") + if haveSupplement: + parseEncodingSupplement(file, encoding, parent.strings) + return encoding def write(self, parent, value): @@ -1743,8 +1746,7 @@ def parseEncodingSupplement(file, encoding, strings): name = strings[sid] encoding[code] = name - -def parseEncoding0(charset, file, haveSupplement, strings): +def parseEncoding0(charset, file): """ Format 0: simple list of codes. After reading the base table, optionally parse the supplement. @@ -1756,13 +1758,12 @@ def parseEncoding0(charset, file, haveSupplement, strings): if code != 0: encoding[code] = charset[glyphID] - if haveSupplement: - parseEncodingSupplement(file, encoding, strings) + 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. @@ -1778,8 +1779,6 @@ def parseEncoding1(charset, file, haveSupplement, strings): code += 1 glyphID += 1 - if haveSupplement: - parseEncodingSupplement(file, encoding, strings) return encoding From 8ba3e4900cd0d3a553f5eb10273e5fab7492e5f5 Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Mon, 28 Apr 2025 01:05:13 +0300 Subject: [PATCH 065/105] lint --- Lib/fontTools/cffLib/__init__.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Lib/fontTools/cffLib/__init__.py b/Lib/fontTools/cffLib/__init__.py index cf18f914b4..4ad724a27a 100644 --- a/Lib/fontTools/cffLib/__init__.py +++ b/Lib/fontTools/cffLib/__init__.py @@ -1674,19 +1674,15 @@ def _read(self, parent, value): fmt = fmt & 0x7F if fmt == 0: - encoding = parseEncoding0( - parent.charset, file - ) + encoding = parseEncoding0(parent.charset, file) elif fmt == 1: - encoding = parseEncoding1( - parent.charset, file - ) + encoding = parseEncoding1(parent.charset, file) else: raise ValueError(f"Unknown Encoding format: {fmt}") if haveSupplement: parseEncodingSupplement(file, encoding, parent.strings) - + return encoding def write(self, parent, value): @@ -1732,6 +1728,7 @@ def readSID(file): 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: @@ -1746,6 +1743,7 @@ def parseEncodingSupplement(file, encoding, strings): name = strings[sid] encoding[code] = name + def parseEncoding0(charset, file): """ Format 0: simple list of codes. @@ -1758,8 +1756,6 @@ def parseEncoding0(charset, file): if code != 0: encoding[code] = charset[glyphID] - - return encoding @@ -1779,7 +1775,6 @@ def parseEncoding1(charset, file): code += 1 glyphID += 1 - return encoding From d5aec1b9dbcbaa3f97d0d553fe85a92927b48faa Mon Sep 17 00:00:00 2001 From: Shimon Doodkin Date: Mon, 28 Apr 2025 01:55:59 +0300 Subject: [PATCH 066/105] add test --- Tests/cffLib/cffLib_test.py | 23 +++++++++++++++---- Tests/cffLib/data/TestSupplementEncoding.cff | Bin 0 -> 1930 bytes 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 Tests/cffLib/data/TestSupplementEncoding.cff diff --git a/Tests/cffLib/cffLib_test.py b/Tests/cffLib/cffLib_test.py index 7146e5d660..8f30fbf26d 100644 --- a/Tests/cffLib/cffLib_test.py +++ b/Tests/cffLib/cffLib_test.py @@ -1,9 +1,15 @@ -from fontTools.cffLib import TopDict, PrivateDict, CharStrings +import os +import sys + +libdir = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))), "Lib" +) +sys.path.insert(0, libdir) + +from fontTools.cffLib import TopDict, PrivateDict, CharStrings, CFFFontSet from fontTools.misc.testTools import parseXML, DataFilesHandler from fontTools.ttLib import TTFont import copy -import os -import sys import unittest from io import BytesIO @@ -119,9 +125,18 @@ def test_unique_glyph_names(self): glyphOrder = font2.getGlyphOrder() self.assertEqual(len(glyphOrder), len(set(glyphOrder))) + def test_reading_supplement_encoding(self): + cff_path = self.getpath("TestSupplementEncoding.cff") + topDict = None + with open(cff_path, "rb") as fontfile: + cff = CFFFontSet() + cff.decompile(fontfile, None) + topDict = cff[0] + self.assertEqual(topDict.Encoding[9], "space") + self.assertEqual(topDict.Encoding[32], "space") -class CFFToCFF2Test(DataFilesHandler): +class CFFToCFF2Test(DataFilesHandler): def test_conversion(self): font_path = self.getpath("CFFToCFF2-1.otf") font = TTFont(font_path) diff --git a/Tests/cffLib/data/TestSupplementEncoding.cff b/Tests/cffLib/data/TestSupplementEncoding.cff new file mode 100644 index 0000000000000000000000000000000000000000..0e43582f004ba32221f71a5ae73e938a62b89e5d GIT binary patch literal 1930 zcmY+ETW}j!8OK*{cC~4O>X1m-yJ0tN2U^Gj)G(C7OoqYGWZF{EW=cqD+Bisd+*;Pn zx=WTEh%m+r?nE2IMe~+G^H6jQ#wp3!vIq}&2SmY1N4Ej&QS_$ zhX*(h=i$tE`Tu|CoX>=8>q1N>)9!CS{@p{59oql&fNNmv@W}z6dvx>obNYQdZQp{f zTTg5>QEJ}w?^CDl>^sfi3*Dgq9&mD(xYwNiB!ZhvpXj;w{?GS){m~(xb70Krb9w^< zBc1`@*zmxJV|Z}TA8;A9hux!|VUN%42>fttcyw@hba*T>;`IBsqW`x$!kD)s=eDmS zH@iMUbmSId+J~4BL)0`^rfE=R|+fTHr3YVYcG=v^O2!4tqn;7KOmlEk0*wxp?EOj zB|X7vJ3(7?OkcM`6ifROo}fG69t(PtIJB9ynW{J^&&g$3R`Jd@VAI#lmcQ%Yw@#PD zmt{&`Xy)fhr8LQFw8m>0HLbG6l*r)X#7uIWi1JZ3La~vF=y(Kr;A)S)9eV?GqwoOs zOJJH^)LQCNud*aAEfEV`lW9;)BUTGlLKUy-D&mssrTmMeKakCY=};!hCKw!IW(z>| z!`7T4Hj0$gSjaDujoA#N(Iuu38za1_ScEk?26-PD*pCTXIaeVU8eFnQ&nD$)fN-;+ zSdvP{f}EFhkLHyTdPIp!0`Vf;B{DVURi-!fN;`9b{KZgDFzTf|%8 z)xjmI<$SL46xsL_3i_}!|3u*`_9(o#BA|jat&r!JQ?VA^idKSNVrbm!Pf&56D?LmG zg1LNz4(BKGlOhhg%@+N6{m)i|JnV-lbm(a0nWu@DM3?HORCnEb)`$Dft|VV4e|%AC zw&`}G-jdJZ^11v{lXyM6GSs4mS|_T{3*aH(?1A;pgVuVXR;f_sa!sm}jT)P%(B-HU z4iJ7Wnlu0>nRF_P^9(b^kjz9*VCV@UmFEh0tiskBgayp$4lA^G94NRC1H8VDLLEB> zyHRi)v*2_-sjJB!72jUO-(Gz6>U+c$b~#?B;`MknuBin@Q8Zl9#M9@9U%6K8rz!YE z=MgJkoRSqn&dGv83G!wTt|%38%T~Nhmt#^SNch=sJWj>qk#vA$n4B>2Uq7tGNm&`s zfo1Dlp{i+=rd10yvQ|wej4zQWM8iZN9Ze*tL?X(C$xy7ICFq2fuFesb-`cBZz3DtRVw+SSAxt`q7}^4LIf%t1T+zES2RK5;TL;vt@yJ#o%OwOyBD zukr99=*LIJ{Z0kmvKz{N==C+Ca3LQxXs9B{##J>W|i{0s;-#kw*-+dDt2)uee`nim#Y z1bD!#X61}TaS|tR)A%%(p5{n+FYLbl9W2a74PGfG$;LA98=%w!`>=n&2ZkA6IOge% zxmkaZ;7_r+B()5`iXMgC;04U`@S5=`!0C>|m@|%_{TdS{jmJw(ioBR9a3YuIa(FJy z#G)^`sh-`t8aEXZueiHt{@V9FC zv%IEr2D>eCQR7 Date: Sun, 4 May 2025 20:04:16 +0300 Subject: [PATCH 067/105] Drop Python 3.8, require 3.9+ See https://github.com/fonttools/fonttools/issues/3665 --- .github/workflows/test.yml | 2 +- Doc/source/index.rst | 2 +- README.rst | 2 +- mypy.ini | 2 +- requirements.txt | 4 ++-- setup.py | 3 +-- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc4733ae5b..ba0a193ee9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: # Oldest supported, 'Stable' supported, Newest supported - python-version: ["3.8", "3.12", "3.13"] + python-version: ["3.9", "3.12", "3.13"] platform: [ubuntu-latest] include: # Only test on the latest supported stable Python on macOS and Windows. - platform: macos-latest diff --git a/Doc/source/index.rst b/Doc/source/index.rst index e98d902a23..e191a33e80 100644 --- a/Doc/source/index.rst +++ b/Doc/source/index.rst @@ -19,7 +19,7 @@ Installation .. note:: - fontTools requires `Python `_ 3.8 or later. + fontTools requires `Python `_ 3.9 or later. To install fontTools, use `pip `_: diff --git a/README.rst b/README.rst index 289cc5a9cb..b94b1d36a6 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ are available at `Read the Docs `_. Installation ~~~~~~~~~~~~ -FontTools requires `Python `__ 3.8 +FontTools requires `Python `__ 3.9 or later. We try to follow the same schedule of minimum Python version support as NumPy (see `NEP 29 `__). 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/requirements.txt b/requirements.txt index 8a834edd2a..bcef14cd21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,8 @@ brotli==1.1.0; platform_python_implementation != "PyPy" brotlicffi==1.1.0.0; platform_python_implementation == "PyPy" unicodedata2==15.1.0; python_version <= '3.11' -scipy==1.10.0; platform_python_implementation != "PyPy" and python_version <= '3.8' # pyup: ignore -scipy==1.14.1; platform_python_implementation != "PyPy" and python_version >= '3.9' +scipy==1.13.1; platform_python_implementation != "PyPy" and python_version <= '3.9' # pyup: ignore +scipy==1.14.1; platform_python_implementation != "PyPy" and python_version >= '3.10' munkres==1.1.4; platform_python_implementation == "PyPy" zopfli==0.2.3.post1 fs==2.4.16 diff --git a/setup.py b/setup.py index 0e07b65680..ec79d26dd2 100755 --- a/setup.py +++ b/setup.py @@ -162,7 +162,6 @@ def doraise_py_compile(file, cfile=None, dfile=None, doraise=False): "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", @@ -504,7 +503,7 @@ def build_extensions(self): url="http://github.com/fonttools/fonttools", license="MIT", platforms=["Any"], - python_requires=">=3.8", + python_requires=">=3.9", long_description=long_description, long_description_content_type="text/x-rst", package_dir={"": "Lib"}, From 856fa514f997e2aa5b098a47f26b9bfc147ba450 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 4 May 2025 20:04:57 +0300 Subject: [PATCH 068/105] Drop some code for ancient Python versions --- Lib/fontTools/misc/etree.py | 31 ++++------------------------- Tests/misc/py23_test.py | 9 +-------- Tests/ttLib/tables/_g_l_y_f_test.py | 1 - Tests/ttLib/tables/tables_test.py | 8 ++------ Tests/varLib/interpolatable_test.py | 3 --- 5 files changed, 7 insertions(+), 45 deletions(-) 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/Tests/misc/py23_test.py b/Tests/misc/py23_test.py index 30382455b7..f31a5b8261 100644 --- a/Tests/misc/py23_test.py +++ b/Tests/misc/py23_test.py @@ -60,15 +60,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/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py index 1deac7606d..9bc3c169f7 100644 --- a/Tests/ttLib/tables/_g_l_y_f_test.py +++ b/Tests/ttLib/tables/_g_l_y_f_test.py @@ -76,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) 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/varLib/interpolatable_test.py b/Tests/varLib/interpolatable_test.py index 92548b9e51..ac3038b1e6 100644 --- a/Tests/varLib/interpolatable_test.py +++ b/Tests/varLib/interpolatable_test.py @@ -94,9 +94,6 @@ def test_interpolatable_otf(self): otf_paths = self.get_file_list(self.tempdir, suffix) self.assertIsNone(interpolatable_main(otf_paths)) - @pytest.mark.skipif( - sys.version_info[:2] == (3, 8), reason="Fails on Python 3.8 for unknown reasons" - ) def test_interpolatable_cff2(self): suffix = ".otf" ttx_dir = self.get_test_input("variable_ttx_interpolatable_cff2") From 40e12b0ac3a4865662917d00d51d9eadd89e1f82 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 4 May 2025 20:15:42 +0300 Subject: [PATCH 069/105] Address TODO(Python 3.7) and TODO(Python 3.8) The future is now. --- Lib/fontTools/otlLib/optimize/gpos.py | 104 +++++++++++--------------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/Lib/fontTools/otlLib/optimize/gpos.py b/Lib/fontTools/otlLib/optimize/gpos.py index 61ea856d96..3edbfeb306 100644 --- a/Lib/fontTools/otlLib/optimize/gpos.py +++ b/Lib/fontTools/otlLib/optimize/gpos.py @@ -1,7 +1,8 @@ import logging import os from collections import defaultdict, namedtuple -from functools import reduce +from dataclasses import dataclass +from functools import cached_property, reduce from itertools import chain from math import log2 from typing import DefaultDict, Dict, Iterable, List, Sequence, Tuple @@ -192,79 +193,58 @@ def _classDef_bytes( ) +@dataclass class Cluster: - # TODO(Python 3.7): Turn this into a dataclass - # ctx: ClusteringContext - # indices: int - # Caches - # TODO(Python 3.8): use functools.cached_property instead of the - # manually cached properties, and remove the cache fields listed below. - # _indices: Optional[List[int]] = None - # _column_indices: Optional[List[int]] = None - # _cost: Optional[int] = None - - __slots__ = "ctx", "indices_bitmask", "_indices", "_column_indices", "_cost" - - def __init__(self, ctx: ClusteringContext, indices_bitmask: int): - self.ctx = ctx - self.indices_bitmask = indices_bitmask - self._indices = None - self._column_indices = None - self._cost = None + ctx: ClusteringContext + indices_bitmask: int - @property + @cached_property def indices(self): - if self._indices is None: - self._indices = bit_indices(self.indices_bitmask) - return self._indices + return bit_indices(self.indices_bitmask) - @property + @cached_property def column_indices(self): - if self._column_indices is None: - # Indices of columns that have a 1 in at least 1 line - # => binary OR all the lines - bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices)) - self._column_indices = bit_indices(bitmask) - return self._column_indices + # Indices of columns that have a 1 in at least 1 line + # => binary OR all the lines + bitmask = reduce(int.__or__, (self.ctx.lines[i] for i in self.indices)) + return bit_indices(bitmask) @property def width(self): # Add 1 because Class2=0 cannot be used but needs to be encoded. return len(self.column_indices) + 1 - @property + @cached_property def cost(self): - if self._cost is None: - self._cost = ( - # 2 bytes to store the offset to this subtable in the Lookup table above - 2 - # Contents of the subtable - # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment - # uint16 posFormat Format identifier: format = 2 - + 2 - # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable. - + 2 - + self.coverage_bytes - # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero). - + 2 - # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero). - + 2 - # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair. - + 2 - + self.classDef1_bytes - # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair. - + 2 - + self.classDef2_bytes - # uint16 class1Count Number of classes in classDef1 table — includes Class 0. - + 2 - # uint16 class2Count Number of classes in classDef2 table — includes Class 0. - + 2 - # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1. - + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes) - * len(self.indices) - * self.width - ) - return self._cost + return ( + # 2 bytes to store the offset to this subtable in the Lookup table above + 2 + # Contents of the subtable + # From: https://docs.microsoft.com/en-us/typography/opentype/spec/gpos#pair-adjustment-positioning-format-2-class-pair-adjustment + # uint16 posFormat Format identifier: format = 2 + + 2 + # Offset16 coverageOffset Offset to Coverage table, from beginning of PairPos subtable. + + 2 + + self.coverage_bytes + # uint16 valueFormat1 ValueRecord definition — for the first glyph of the pair (may be zero). + + 2 + # uint16 valueFormat2 ValueRecord definition — for the second glyph of the pair (may be zero). + + 2 + # Offset16 classDef1Offset Offset to ClassDef table, from beginning of PairPos subtable — for the first glyph of the pair. + + 2 + + self.classDef1_bytes + # Offset16 classDef2Offset Offset to ClassDef table, from beginning of PairPos subtable — for the second glyph of the pair. + + 2 + + self.classDef2_bytes + # uint16 class1Count Number of classes in classDef1 table — includes Class 0. + + 2 + # uint16 class2Count Number of classes in classDef2 table — includes Class 0. + + 2 + # Class1Record class1Records[class1Count] Array of Class1 records, ordered by classes in classDef1. + + (self.ctx.valueFormat1_bytes + self.ctx.valueFormat2_bytes) + * len(self.indices) + * self.width + ) @property def coverage_bytes(self): From 9a7ffb6612d1ea44dd07f84e2290dae1fbf521e0 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 4 May 2025 20:21:18 +0300 Subject: [PATCH 070/105] Address TODO(Python 3.8) The future is now. --- Lib/fontTools/designspaceLib/statNames.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/designspaceLib/statNames.py b/Lib/fontTools/designspaceLib/statNames.py index a5c02b3f60..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( @@ -205,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. From c537154c887fdc2e37a34f097c21c352b55783d5 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 4 May 2025 20:25:27 +0300 Subject: [PATCH 071/105] Remove mention of enum34 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We don’t use it since Python 3.4 support was dropped. --- Doc/source/optional.rst | 2 -- README.rst | 3 --- 2 files changed, 5 deletions(-) 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/README.rst b/README.rst index b94b1d36a6..e40554dae8 100644 --- a/README.rst +++ b/README.rst @@ -88,9 +88,6 @@ are required to unlock the extra features named "ufo", etc. * `fs `__: (aka ``pyfilesystem2``) filesystem abstraction layer. - * `enum34 `__: backport for the built-in ``enum`` - module (only required on Python < 3.4). - *Extra:* ``ufo`` - ``Lib/fontTools/ttLib/woff2.py`` From d85f057fe30bb72f31c4f70db778cfb2cb2d350d Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 22:15:48 +0200 Subject: [PATCH 072/105] [voltToFea] Improve tests Swap the actual and expected strings in self.assertEqual() calls so that the diffs are shown correctly. --- Tests/voltLib/volttofea_test.py | 104 ++++++++++++++++---------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 0d8d8d289b..ec1d9fadc7 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -30,11 +30,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 +42,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 +89,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 +105,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 +124,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 +150,13 @@ 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_glyphs_and_group(self): @@ -171,11 +171,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,7 +195,6 @@ def test_def_group_range(self): "END_GROUP" ) self.assertEqual( - fea, "# Glyph classes\n" "@KERN_lc_a_2ND = [a - atilde b c - cdotaccent];\n" "@GDEF_base = [a agrave aacute acircumflex atilde c" @@ -203,11 +202,12 @@ def test_def_group_range(self): "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 +218,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 +227,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 +244,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 +253,7 @@ def test_langsys_no_lang_name(self): "END_LANGSYS\n" "END_SCRIPT" ) - self.assertEqual(fea, "") + self.assertEqual("", fea) def test_feature(self): fea = self.parse( @@ -275,7 +275,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 +286,7 @@ def test_feature(self): " language ROM exclude_dflt;\n" " lookup fraclookup;\n" "} frac;\n", + fea, ) def test_feature_sub_lookups(self): @@ -319,7 +319,6 @@ def test_feature_sub_lookups(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup fraclookup {\n" " lookupflag RightToLeft;\n" @@ -336,6 +335,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 +355,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 +382,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,7 +409,6 @@ def test_substitution_single_in_context(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@Denominators = [one.dnom two.dnom];\n" "\n" @@ -418,6 +417,7 @@ def test_substitution_single_in_context(self): " sub [@Denominators fraction] one' by one.dnom;\n" " sub [@Denominators fraction] two' by two.dnom;\n" "} fracdnom;\n", + fea, ) def test_substitution_single_in_contexts(self): @@ -441,7 +441,6 @@ def test_substitution_single_in_contexts(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@Hebrew = [uni05D0 uni05D1];\n" "\n" @@ -450,6 +449,7 @@ def test_substitution_single_in_contexts(self): " sub dollar' @Hebrew one.Hebr by dollar.Hebr;\n" " sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n" "} HebrewCurrency;\n", + fea, ) def test_substitution_single_except_context(self): @@ -473,7 +473,6 @@ def test_substitution_single_except_context(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@Hebrew = [uni05D0 uni05D1];\n" "\n" @@ -482,6 +481,7 @@ def test_substitution_single_except_context(self): " ignore sub dollar' @Hebrew one.Hebr;\n" " sub @Hebrew one.Hebr dollar' by dollar.Hebr;\n" "} HebrewCurrency;\n", + fea, ) def test_substitution_skip_base(self): @@ -499,7 +499,6 @@ def test_substitution_skip_base(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -508,6 +507,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 +525,6 @@ def test_substitution_process_base(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -533,6 +532,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 +550,6 @@ def test_substitution_process_marks_all(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -558,6 +557,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 +575,6 @@ def test_substitution_process_marks_none(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -584,6 +583,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 +601,6 @@ def test_substitution_skip_marks(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [marka markb];\n" "\n" @@ -610,6 +609,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 +626,6 @@ def test_substitution_mark_attachment(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [acutecmb gravecmb];\n" "\n" @@ -636,6 +635,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 +652,6 @@ def test_substitution_mark_glyph_set(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [acutecmb gravecmb];\n" "\n" @@ -662,6 +661,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 +678,6 @@ def test_substitution_process_all_marks(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "# Glyph classes\n" "@SomeMarks = [acutecmb gravecmb];\n" "\n" @@ -687,6 +686,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 +704,11 @@ def test_substitution_no_reversal(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup Lookup {\n" " sub a' [a b] by a.alt;\n" "} Lookup;\n", + fea, ) def test_substitution_reversal(self): @@ -731,7 +731,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 +739,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 +758,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,12 +782,12 @@ def test_substitution_multiple_to_single(self): "END_SUBSTITUTION" ) self.assertEqual( - fea, "\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_reverse_chaining_single(self): @@ -807,11 +807,11 @@ 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", + fea, ) # GPOS @@ -841,7 +841,6 @@ 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" @@ -854,6 +853,7 @@ def test_position_attach(self): " pos base e\n" " mark @top;\n" "} anchor_top;\n", + fea, ) def test_position_attach_mkmk(self): @@ -875,7 +875,6 @@ 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" "\n" @@ -890,6 +889,7 @@ def test_position_attach_mkmk(self): "table GDEF {\n" " GlyphClassDef , , @GDEF_mark, ;\n" "} GDEF;\n", + fea, ) def test_position_attach_in_context(self): @@ -911,7 +911,6 @@ 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" @@ -927,6 +926,7 @@ def test_position_attach_in_context(self): " ignore pos a [acutecomb gravecomb]';\n" " pos [acutecomb gravecomb]' lookup test_target;\n" "} test;\n", + fea, ) def test_position_attach_cursive(self): @@ -946,7 +946,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 +953,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 +972,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,7 +995,6 @@ def test_position_adjust_pair_in_context(self): "END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup kern1_target {\n" " enum pos V A -25;\n" @@ -1005,6 +1004,7 @@ def test_position_adjust_pair_in_context(self): " ignore pos A V' A';\n" " pos V' lookup kern1_target A' lookup kern1_target;\n" "} kern1;\n", + fea, ) def test_position_adjust_single(self): @@ -1021,12 +1021,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,7 +1045,6 @@ def test_position_adjust_single_in_context(self): "END_POSITION\n" ) self.assertEqual( - fea, "\n# Lookups\n" "lookup TestLookup_target {\n" " pos glyph1 <123 0 0 0>;\n" @@ -1056,6 +1055,7 @@ def test_position_adjust_single_in_context(self): " ignore pos leftGlyph [glyph1 glyph2]' rightGlyph;\n" " pos [glyph1 glyph2]' lookup TestLookup_target;\n" "} TestLookup;\n", + fea, ) def test_def_anchor(self): @@ -1075,7 +1075,6 @@ 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" "\n" @@ -1084,6 +1083,7 @@ def test_def_anchor(self): " pos base a\n" " mark @top;\n" "} TestLookup;\n", + fea, ) def test_def_anchor_multi_component(self): @@ -1106,7 +1106,6 @@ def test_def_anchor_multi_component(self): "COMPONENT 1 AT POS END_POS END_ANCHOR" ) self.assertEqual( - fea, "\n# Mark classes\n" "markClass acutecomb @top;\n" "\n" @@ -1122,6 +1121,7 @@ def test_def_anchor_multi_component(self): "table GDEF {\n" " GlyphClassDef , @GDEF_ligature, , ;\n" "} GDEF;\n", + fea, ) def test_anchor_adjust_device(self): @@ -1131,10 +1131,10 @@ def test_anchor_adjust_device(self): "ADJUST_BY 56 AT 78 END_POS END_ANCHOR" ) self.assertEqual( - fea, "\n# Mark classes\n" "#markClass diacglyph " " > @top;", + fea, ) def test_use_extension(self): @@ -1154,18 +1154,18 @@ def test_use_extension(self): "COMPILER_USEEXTENSIONLOOKUPS\n" ) self.assertEqual( - fea, "\n# Lookups\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 +1183,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 +1191,7 @@ def test_sanitize_lookup_name(self): "lookup Test_Lookup_ {\n" " \n" "} Test_Lookup_;\n", + fea, ) def test_sanitize_group_name(self): @@ -1204,10 +1204,10 @@ 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_cli_vtp(self): From fdda2c7781f62ab627963bd4613b8c655803256b Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 15 Mar 2025 22:15:21 +0200 Subject: [PATCH 073/105] [voltToFea] Correctly handle nested enums inside group definition --- Lib/fontTools/voltLib/voltToFea.py | 9 ++++++--- Tests/voltLib/volttofea_test.py | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index c77d5ad111..ab6f8ec3dd 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -253,7 +253,7 @@ def _groupName(self, group): name = group return ast.GlyphClassName(self._glyphclasses[name.lower()]) - def _coverage(self, coverage): + def _coverage(self, coverage, flatten=False): items = [] for item in coverage: if isinstance(item, VAst.GlyphName): @@ -261,7 +261,10 @@ def _coverage(self, coverage): elif isinstance(item, VAst.GroupName): items.append(self._groupName(item)) elif isinstance(item, VAst.Enum): - items.append(self._enum(item)) + if flatten: + items.extend(item.glyphSet()) + else: + items.append(self._enum(item)) elif isinstance(item, VAst.Range): items.append((item.start, item.end)) else: @@ -269,7 +272,7 @@ def _coverage(self, coverage): return items def _enum(self, enum): - return ast.GlyphClass(self._coverage(enum.enum)) + return ast.GlyphClass(self._coverage(enum.enum, flatten=True)) def _context(self, context): out = [] diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index ec1d9fadc7..6874270fcf 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -1210,6 +1210,14 @@ def test_sanitize_group_name(self): 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_cli_vtp(self): vtp = DATADIR / "Nutso.vtp" fea = DATADIR / "Nutso.fea" From 73a701934ff0a88b32f953af9ae83a630d6cf518 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 16 Mar 2025 01:19:32 +0200 Subject: [PATCH 074/105] [voltLib] Handle alternate substitution See inline comments and the example in the test. --- Lib/fontTools/voltLib/ast.py | 4 ++++ Lib/fontTools/voltLib/parser.py | 10 ++++++++- Lib/fontTools/voltLib/voltToFea.py | 24 ++++++++++++++++++++++ Tests/voltLib/volttofea_test.py | 33 ++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/voltLib/ast.py b/Lib/fontTools/voltLib/ast.py index 82c2cca8b7..dba8f4d45b 100644 --- a/Lib/fontTools/voltLib/ast.py +++ b/Lib/fontTools/voltLib/ast.py @@ -317,6 +317,10 @@ class SubstitutionLigatureDefinition(SubstitutionDefinition): pass +class SubstitutionAlternateDefinition(SubstitutionDefinition): + pass + + class SubstitutionReverseChainingSingleDefinition(SubstitutionDefinition): pass diff --git a/Lib/fontTools/voltLib/parser.py b/Lib/fontTools/voltLib/parser.py index 1fa6b11d02..fd2aed65be 100644 --- a/Lib/fontTools/voltLib/parser.py +++ b/Lib/fontTools/voltLib/parser.py @@ -325,7 +325,15 @@ def parse_substitution_(self, reversal): mapping, location=location ) else: - sub = ast.SubstitutionSingleDefinition(mapping, location=location) + # Alternate substitutions are represented by adding multiple + # substitutions for the same glyph, so we detect that here + glyphs = [x.glyphSet() for cov in src for x in cov] # flatten src + if len(set(glyphs)) != len(glyphs): # src has duplicates + sub = ast.SubstitutionAlternateDefinition( + mapping, location=location + ) + else: + sub = ast.SubstitutionSingleDefinition(mapping, location=location) elif max_src == 1 and max_dest > 1: sub = ast.SubstitutionMultipleDefinition(mapping, location=location) elif max_src > 1 and max_dest == 1: diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index ab6f8ec3dd..c6d3bb218b 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -540,6 +540,30 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): statements = fealookup.statements sub = lookup.sub + + # Alternate substitutions are represented by adding multiple + # substitutions for the same glyph, so we need to collect them into one + # to many mapping. + if isinstance(sub, VAst.SubstitutionAlternateDefinition): + alternates = {} + for key, val in sub.mapping.items(): + glyphs = self._coverage(key) + replacements = self._coverage(val) + assert len(glyphs) == 1 + for src_glyph, repl_glyph in zip( + glyphs[0].glyphSet(), replacements[0].glyphSet() + ): + alternates.setdefault(str(self._glyphName(src_glyph)), []).append( + str(self._glyphName(repl_glyph)) + ) + + for glyph, replacements in alternates.items(): + statement = ast.AlternateSubstStatement( + prefix, glyph, suffix, ast.GlyphClass(replacements), chain + ) + statements.append(statement) + return + for key, val in sub.mapping.items(): if not key or not val: path, line, column = sub.location diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 6874270fcf..498affff5e 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -814,6 +814,39 @@ def test_substitution_reverse_chaining_single(self): 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 # ATTACH_CURSIVE # ATTACH From a90a3c07c39338d3d46455ba731219de5368a10c Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 1 Apr 2025 03:08:58 +0200 Subject: [PATCH 075/105] [voltToFea] Fix anchor looks mismatch between how VOLT and feature files See inline comments. --- Lib/fontTools/voltLib/voltToFea.py | 75 +++--- Tests/voltLib/data/NamdhinggoSIL1006.fea | 316 +++++++++++------------ Tests/voltLib/data/Nutso.fea | 238 ++++++++--------- Tests/voltLib/volttofea_test.py | 127 +++++++-- 4 files changed, 427 insertions(+), 329 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index c6d3bb218b..f9755480ac 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -57,15 +57,6 @@ TABLES = ["GDEF", "GSUB", "GPOS"] -class MarkClassDefinition(ast.MarkClassDefinition): - def asFea(self, indent=""): - res = "" - if not getattr(self, "used", False): - res += "#" - res += ast.MarkClassDefinition.asFea(self, indent) - return res - - # For sorting voltLib.ast.GlyphDefinition, see its use below. class Group: def __init__(self, group): @@ -361,18 +352,11 @@ def _anchorDefinition(self, anchordef): glyphname = anchordef.glyph_name anchor = self._anchor(anchordef.pos) - if anchorname.startswith("MARK_"): - name = "_".join(anchorname.split("_")[1:]) - markclass = ast.MarkClass(self._className(name)) - glyph = self._glyphName(glyphname) - markdef = MarkClassDefinition(markclass, anchor, glyph) - self._markclasses[(glyphname, anchorname)] = markdef - else: - if glyphname not in self._anchors: - self._anchors[glyphname] = {} - if anchorname not in self._anchors[glyphname]: - self._anchors[glyphname][anchorname] = {} - self._anchors[glyphname][anchorname][anchordef.component] = anchor + if glyphname not in self._anchors: + self._anchors[glyphname] = {} + if anchorname not in self._anchors[glyphname]: + self._anchors[glyphname][anchorname] = {} + self._anchors[glyphname][anchorname][anchordef.component] = anchor def _gposLookup(self, lookup, fealookup): statements = fealookup.statements @@ -411,22 +395,43 @@ def _gposLookup(self, lookup, fealookup): ) elif isinstance(pos, VAst.PositionAttachDefinition): anchors = {} - for marks, classname in pos.coverage_to: - for mark in marks: - # Set actually used mark classes. Basically a hack to get - # around the feature file syntax limitation of making mark - # classes global and not allowing mark positioning to - # specify mark coverage. - for name in mark.glyphSet(): - key = (name, "MARK_" + classname) - self._markclasses[key].used = True - markclass = ast.MarkClass(self._className(classname)) + allmarks = set() + for coverage, anchorname in pos.coverage_to: + # In feature files mark classes are global, but in VOLT they + # are defined per-lookup. If we output mark class definitions + # for all marks that use a given anchor, we might end up with a + # mark used in two different classes in the same lookup, which + # is causes feature file compilation error. + # At the expense of uglier feature code, we make the mark class + # name by appending the current lookup name not the anchor + # name, and output mark class definitions only for marks used + # in this lookup. + classname = self._className(f"{anchorname}.{lookup.name}") + markclass = ast.MarkClass(classname) + + # We might still end in marks used in two different anchor + # classes, so we filter out already used marks. + marks = set() + for mark in coverage: + marks.update(mark.glyphSet()) + if not marks.isdisjoint(allmarks): + marks.difference_update(allmarks) + if not marks: + continue + allmarks.update(marks) + + for glyphname in marks: + glyph = self._glyphName(glyphname) + anchor = self._anchors[glyphname][f"MARK_{anchorname}"][1] + markdef = ast.MarkClassDefinition(markclass, anchor, glyph) + self._markclasses[(glyphname, classname)] = markdef + for base in pos.coverage: for name in base.glyphSet(): if name not in anchors: anchors[name] = [] - if classname not in anchors[name]: - anchors[name].append(classname) + if (anchorname, classname) not in anchors[name]: + anchors[name].append((anchorname, classname)) for name in anchors: components = 1 @@ -434,8 +439,8 @@ def _gposLookup(self, lookup, fealookup): components = self._ligatures[name] marks = [] - for mark in anchors[name]: - markclass = ast.MarkClass(self._className(mark)) + for mark, classname in anchors[name]: + markclass = ast.MarkClass(classname) for component in range(1, components + 1): if len(marks) < component: marks.append([]) diff --git a/Tests/voltLib/data/NamdhinggoSIL1006.fea b/Tests/voltLib/data/NamdhinggoSIL1006.fea index aa8ab1a5de..34ab500c0d 100644 --- a/Tests/voltLib/data/NamdhinggoSIL1006.fea +++ b/Tests/voltLib/data/NamdhinggoSIL1006.fea @@ -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 { @@ -111,326 +111,326 @@ lookup 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..73bfbdbdbf 100644 --- a/Tests/voltLib/data/Nutso.fea +++ b/Tests/voltLib/data/Nutso.fea @@ -5,106 +5,106 @@ @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 { @@ -132,47 +132,47 @@ lookup kern.numeral_to_fraction { lookup fracmark.init_1.10_target { pos base fracinit - mark @INIT.1.10; + mark @INIT.1.10.fracmark.init_1.10; } fracmark.init_1.10_target; lookup fracmark.init_2.10_target { pos base fracinit - mark @INIT.2.10; + mark @INIT.2.10.fracmark.init_2.10; } fracmark.init_2.10_target; lookup fracmark.init_3.10_target { pos base fracinit - mark @INIT.3.10; + mark @INIT.3.10.fracmark.init_3.10; } fracmark.init_3.10_target; lookup fracmark.init_4.10_target { pos base fracinit - mark @INIT.4.10; + mark @INIT.4.10.fracmark.init_4.10; } fracmark.init_4.10_target; lookup fracmark.init_5.10_target { pos base fracinit - mark @INIT.5.10; + mark @INIT.5.10.fracmark.init_5.10; } fracmark.init_5.10_target; lookup fracmark.init_6.10_target { pos base fracinit - mark @INIT.6.10; + mark @INIT.6.10.fracmark.init_6.10; } fracmark.init_6.10_target; lookup fracmark.init_7.10_target { pos base fracinit - mark @INIT.7.10; + mark @INIT.7.10.fracmark.init_7.10; } fracmark.init_7.10_target; lookup fracmark.init_8.10_target { pos base fracinit - mark @INIT.8.10; + mark @INIT.8.10.fracmark.init_8.10; } fracmark.init_8.10_target; lookup fracmark.init_9.10_target { pos base fracinit - mark @INIT.9.10; + mark @INIT.9.10.fracmark.init_9.10; } fracmark.init_9.10_target; lookup fracmark.init { @@ -242,25 +242,25 @@ lookup 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/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 498affff5e..77bd2716a6 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -10,6 +10,8 @@ class ToFeaTest(unittest.TestCase): + maxDiff = 10000000 + @classmethod def setup_class(cls): cls.tempdir = None @@ -875,16 +877,16 @@ def test_position_attach(self): ) self.assertEqual( "\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, ) @@ -909,13 +911,13 @@ def test_position_attach_mkmk(self): ) self.assertEqual( "\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" @@ -945,13 +947,13 @@ def test_position_attach_in_context(self): ) self.assertEqual( "\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" " pos base a\n" - " mark @top;\n" + " mark @top.test;\n" "} test_target;\n" "\n" "lookup test {\n" @@ -962,6 +964,83 @@ def test_position_attach_in_context(self): 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): fea = self.parse( 'DEF_LOOKUP "SomeLookup" PROCESS_BASE PROCESS_MARKS ALL ' @@ -1109,12 +1188,12 @@ def test_def_anchor(self): ) self.assertEqual( "\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, ) @@ -1140,14 +1219,14 @@ def test_def_anchor_multi_component(self): ) 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" + " mark @top.TestLookup;\n" "} TestLookup;\n" "\n" "@GDEF_ligature = [f_f];\n" @@ -1161,12 +1240,26 @@ 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( "\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, ) From be235f7152bb908467d663dd417a24d018339a27 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 15 Apr 2025 00:44:06 +0200 Subject: [PATCH 076/105] [voltToFea] Make anchor names case-insensitive This seems to be what VOLT does. --- Lib/fontTools/voltLib/voltToFea.py | 7 ++++++ Tests/voltLib/volttofea_test.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index f9755480ac..a46b90514f 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -354,6 +354,10 @@ def _anchorDefinition(self, anchordef): if glyphname not in self._anchors: self._anchors[glyphname] = {} + if anchorname.startswith("MARK_"): + anchorname = anchorname[:5] + anchorname[5:].lower() + else: + anchorname = anchorname.lower() if anchorname not in self._anchors[glyphname]: self._anchors[glyphname][anchorname] = {} self._anchors[glyphname][anchorname][anchordef.component] = anchor @@ -409,6 +413,9 @@ def _gposLookup(self, lookup, fealookup): classname = self._className(f"{anchorname}.{lookup.name}") markclass = ast.MarkClass(classname) + # Anchor names are case-insensitive in VOLT + anchorname = anchorname.lower() + # We might still end in marks used in two different anchor # classes, so we filter out already used marks. marks = set() diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 77bd2716a6..adf7685fd6 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 @@ -1198,6 +1199,41 @@ def test_def_anchor(self): 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): fea = self.parse( 'DEF_LOOKUP "TestLookup" PROCESS_BASE PROCESS_MARKS ALL ' From 7e0872923ca5a3e216d2ef19a1cf15ad294fd437 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 15 Apr 2025 02:21:45 +0200 Subject: [PATCH 077/105] [voltToFea] Handle groups in ligature substitution If any of the input glyphs is a glyph class, we need to explode it into multiple statements since feature file syntax does not support classes in ligature substitutions. --- Lib/fontTools/voltLib/voltToFea.py | 33 +++++++++++ Tests/voltLib/data/NamdhinggoSIL1006.fea | 62 ++++++++++++++++++++- Tests/voltLib/data/Nutso.fea | 24 +++++++- Tests/voltLib/volttofea_test.py | 71 ++++++++++++++++++++++++ 4 files changed, 186 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index a46b90514f..75bb75c6ce 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -609,6 +609,39 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): statement = ast.LigatureSubstStatement( prefix, glyphs, suffix, replacements[0], chain ) + + # If any of the input glyphs is a group, we need to + # explode the substitution into multiple ligature substitutions + # since feature file syntax does not support classes in + # ligature substitutions. + n = max(len(x.glyphSet()) for x in glyphs) + if n > 1: + # All input should either be groups of the same length or single glyphs + assert all(len(x.glyphSet()) in (n, 1) for x in glyphs) + glyphs = [x.glyphSet() for x in glyphs] + glyphs = [([x[0]] * n if len(x) == 1 else x) for x in glyphs] + + # In this case ligature replacements must be a group of the same length + # as the input groups, or a single glyph. VOLT + # allows the replacement glyphs to be longer and truncates them. + # So well allow that and zip() below will do the truncation + # for us. + replacement = replacements[0].glyphSet() + if len(replacement) == 1: + replacement = [replacement[0]] * n + assert len(replacement) >= n + + # Add the unexploded statement commented out for reference. + statements.append(ast.Comment(f"# {statement}")) + + for zipped in zip(*glyphs, replacement): + zipped = [self._glyphName(x) for x in zipped] + statements.append( + ast.LigatureSubstStatement( + prefix, zipped[:-1], suffix, zipped[-1], chain + ) + ) + continue else: raise NotImplementedError(sub) statements.append(statement) diff --git a/Tests/voltLib/data/NamdhinggoSIL1006.fea b/Tests/voltLib/data/NamdhinggoSIL1006.fea index 34ab500c0d..153090b487 100644 --- a/Tests/voltLib/data/NamdhinggoSIL1006.fea +++ b/Tests/voltLib/data/NamdhinggoSIL1006.fea @@ -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; diff --git a/Tests/voltLib/data/Nutso.fea b/Tests/voltLib/data/Nutso.fea index 73bfbdbdbf..3e255d7957 100644 --- a/Tests/voltLib/data/Nutso.fea +++ b/Tests/voltLib/data/Nutso.fea @@ -116,8 +116,28 @@ lookup frac.dnom { } 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 { diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index adf7685fd6..3aad434ed3 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -793,6 +793,77 @@ def test_substitution_multiple_to_single(self): 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_LOOKUP "numr" PROCESS_BASE PROCESS_MARKS ALL ' From df3bd91d0e4f76f526cf026a480f228e8a53f70e Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Wed, 16 Apr 2025 05:03:26 +0200 Subject: [PATCH 078/105] [voltToFea] Handle mark to ligature anchors with missing components --- Lib/fontTools/voltLib/voltToFea.py | 7 ++---- Tests/voltLib/volttofea_test.py | 36 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 75bb75c6ce..f96bbbcc5d 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -445,16 +445,13 @@ def _gposLookup(self, lookup, fealookup): if name in self._ligatures: components = self._ligatures[name] - marks = [] + marks = [[] for _ in range(components)] for mark, classname in anchors[name]: markclass = ast.MarkClass(classname) for component in range(1, components + 1): - if len(marks) < component: - marks.append([]) - anchor = None if component in self._anchors[name][mark]: anchor = self._anchors[name][mark][component] - marks[component - 1].append((anchor, markclass)) + marks[component - 1].append((anchor, markclass)) base = self._glyphName(name) if name in self._marks: diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 3aad434ed3..f627767c3f 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -1343,6 +1343,42 @@ def test_def_anchor_multi_component(self): 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.TestLookup;\n" + "\n" + "# Lookups\n" + "lookup TestLookup {\n" + " pos ligature f_f\n" + " mark @top.TestLookup\n" + " ligComponent\n" + " ;\n" + "} TestLookup;\n" + "\n" + "@GDEF_ligature = [f_f];\n" + "table GDEF {\n" + " GlyphClassDef , @GDEF_ligature, , ;\n" + "} GDEF;\n", + fea, + ) + def test_anchor_adjust_device(self): fea = self.parse( 'DEF_ANCHOR "MARK_top" ON 123 GLYPH diacglyph ' From 09231fa2d17f65af51b488ccdd2aaf4b4e4c276e Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Wed, 16 Apr 2025 16:47:43 +0200 Subject: [PATCH 079/105] [voltToFea] Fix handling of ranges VOLT ranges are GID-based, while feature files ranges are glyph names-based, so the two are not compatible and we should expand VOLT ranges. --- Lib/fontTools/voltLib/voltToFea.py | 5 ++++- Tests/voltLib/volttofea_test.py | 32 +++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index f96bbbcc5d..37dde2ac03 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -257,7 +257,10 @@ def _coverage(self, coverage, flatten=False): else: items.append(self._enum(item)) elif isinstance(item, VAst.Range): - items.append((item.start, item.end)) + if flatten: + items.extend(item.glyphSet()) + else: + items.append(ast.GlyphClass(item.glyphSet())) else: raise NotImplementedError(item) return items diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index f627767c3f..5946033549 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -199,7 +199,7 @@ def test_def_group_range(self): ) self.assertEqual( "# 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" @@ -866,6 +866,26 @@ def test_substitution_multiple_to_single_with_enums_long_replacement(self): 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" @@ -883,8 +903,14 @@ def test_substitution_reverse_chaining_single(self): self.assertEqual( "\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, ) From 447a3f967e0cbc01d9a1fff55db7002944ad9b38 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Wed, 16 Apr 2025 18:13:48 +0200 Subject: [PATCH 080/105] [voltToFea] Process glyph definitions before groups Since we need to map VOLT glyph names to font glyph names and groups reference glyphs. --- Lib/fontTools/voltLib/voltToFea.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 37dde2ac03..4e9ab08d9f 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -120,6 +120,11 @@ def _className(self, name): return self._class_names[name] def _collectStatements(self, doc, tables): + # Collect glyph difinitions first, as we need them to map VOLT glyph names to font glyph name. + for statement in doc.statements: + if isinstance(statement, VAst.GlyphDefinition): + self._glyphDefinition(statement) + # Collect and sort group definitions first, to make sure a group # definition that references other groups comes after them since VOLT # does not enforce such ordering, and feature file require it. @@ -128,14 +133,12 @@ def _collectStatements(self, doc, tables): self._groupDefinition(statement) for statement in doc.statements: - if isinstance(statement, VAst.GlyphDefinition): - self._glyphDefinition(statement) - elif isinstance(statement, VAst.AnchorDefinition): + if isinstance(statement, VAst.AnchorDefinition): if "GPOS" in tables: self._anchorDefinition(statement) elif isinstance(statement, VAst.SettingDefinition): self._settingDefinition(statement) - elif isinstance(statement, VAst.GroupDefinition): + elif isinstance(statement, (VAst.GlyphDefinition, VAst.GroupDefinition)): pass # Handled above elif isinstance(statement, VAst.ScriptDefinition): self._scriptDefinition(statement) From 490102919536de06f3c46f4e7fdb943865410db4 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 17 Apr 2025 00:40:11 +0200 Subject: [PATCH 081/105] [voltToFea] Fix detecting mark to ligature lookups If only some of the glyphs are ligatures or marks, then VOLT makes it a mark to base lookup and uses the anchor of the first ligature component (in case of ligatures). --- Lib/fontTools/voltLib/voltToFea.py | 8 +-- Tests/voltLib/volttofea_test.py | 85 ++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 4e9ab08d9f..1908af5a3f 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -446,9 +446,11 @@ def _gposLookup(self, lookup, fealookup): if (anchorname, classname) not in anchors[name]: anchors[name].append((anchorname, classname)) + is_ligature = all(n in self._ligatures for n in anchors) + is_mark = all(n in self._marks for n in anchors) for name in anchors: components = 1 - if name in self._ligatures: + if is_ligature: components = self._ligatures[name] marks = [[] for _ in range(components)] @@ -460,9 +462,9 @@ def _gposLookup(self, lookup, fealookup): marks[component - 1].append((anchor, markclass)) base = self._glyphName(name) - if name in self._marks: + if is_mark: mark = ast.MarkMarkPosStatement(base, marks[0]) - elif name in self._ligatures: + elif is_ligature: mark = ast.MarkLigPosStatement(base, marks) else: mark = ast.MarkBasePosStatement(base, marks[0]) diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 5946033549..a8b60aaf56 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -1405,6 +1405,91 @@ def test_def_anchor_ligature_missing_component(self): 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 ' From c6a193a0b2592f029056aa3735df248ceaff6789 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 21:22:36 +0200 Subject: [PATCH 082/105] [voltToFea] Fix sorting group definitions In VOLT group definitions can be ordered in any order, but in FEA a group must be defined before being referenced in another group. We had some code to sort the groups to fix this, but it was broken in several ways. We now use the more appropriate TopologicalSorter(), and nested enums when getting what groups a group references. --- Lib/fontTools/voltLib/voltToFea.py | 47 +++++++++++++++--------- Tests/voltLib/data/NamdhinggoSIL1006.fea | 2 +- Tests/voltLib/volttofea_test.py | 33 +++++++++++++++++ 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 1908af5a3f..987c161c3a 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -46,6 +46,7 @@ import logging import re from io import StringIO +from graphlib import TopologicalSorter from fontTools.feaLib import ast from fontTools.ttLib import TTFont, TTLibError @@ -57,23 +58,33 @@ TABLES = ["GDEF", "GSUB", "GPOS"] -# For sorting voltLib.ast.GlyphDefinition, see its use below. -class Group: - def __init__(self, group): - self.name = group.name.lower() - self.groups = [ - x.group.lower() for x in group.enum.enum if isinstance(x, VAst.GroupName) +def _flatten_group(group): + ret = [] + if isinstance(group, (tuple, list)): + for item in group: + ret.extend(_flatten_group(item)) + elif hasattr(group, "enum"): + ret.extend(_flatten_group(group.enum)) + else: + ret.append(group) + return ret + + +# Topologically sort of group definitions to ensure that all groups are defined +# before they are referenced. This is necessary because FEA requires it but +# VOLT does not, see below. +def sort_groups(groups): + group_map = {group.name.lower(): group for group in groups} + graph = { + group.name.lower(): [ + x.group.lower() + for x in _flatten_group(group) + if isinstance(x, VAst.GroupName) ] - - def __lt__(self, other): - if self.name in other.groups: - return True - if other.name in self.groups: - return False - if self.groups and not other.groups: - return False - if not self.groups and other.groups: - return True + for group in groups + } + sorter = TopologicalSorter(graph) + return [group_map[name] for name in sorter.static_order()] class VoltToFea: @@ -129,8 +140,8 @@ def _collectStatements(self, doc, tables): # definition that references other groups comes after them since VOLT # does not enforce such ordering, and feature file require it. groups = [s for s in doc.statements if isinstance(s, VAst.GroupDefinition)] - for statement in sorted(groups, key=lambda x: Group(x)): - self._groupDefinition(statement) + for group in sort_groups(groups): + self._groupDefinition(group) for statement in doc.statements: if isinstance(statement, VAst.AnchorDefinition): diff --git a/Tests/voltLib/data/NamdhinggoSIL1006.fea b/Tests/voltLib/data/NamdhinggoSIL1006.fea index 153090b487..0aa5ddd357 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]; diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index a8b60aaf56..f67424ffbe 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -162,6 +162,39 @@ def test_def_group_groups_not_yet_defined(self): 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): fea = self.parse( 'DEF_GROUP "aaccented"\n' From e44540015d42792fc12f5528e69b18831a17e59d Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 20 Apr 2025 22:43:56 +0200 Subject: [PATCH 083/105] [voltToFea] Fix nested enums Without this fix we would output invalid fea code like: pos [[a b] [c d] [e f g h i j k l] [m n o p q]]' lookup lookup_target space; --- Lib/fontTools/voltLib/voltToFea.py | 41 +++++++++++++---------- Tests/voltLib/volttofea_test.py | 54 ++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 987c161c3a..bf9b937d0b 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -258,6 +258,12 @@ def _groupName(self, group): name = group return ast.GlyphClassName(self._glyphclasses[name.lower()]) + def _glyphSet(self, item): + return [ + (self._glyphName(x) if isinstance(x, (str, VAst.GlyphName)) else x) + for x in item.glyphSet() + ] + def _coverage(self, coverage, flatten=False): items = [] for item in coverage: @@ -266,37 +272,38 @@ def _coverage(self, coverage, flatten=False): elif isinstance(item, VAst.GroupName): items.append(self._groupName(item)) elif isinstance(item, VAst.Enum): + item = self._coverage(item.enum, flatten=True) if flatten: - items.extend(item.glyphSet()) + items.extend(item) else: - items.append(self._enum(item)) + items.append(ast.GlyphClass(item)) elif isinstance(item, VAst.Range): + item = self._glyphSet(item) if flatten: - items.extend(item.glyphSet()) + items.extend(item) else: - items.append(ast.GlyphClass(item.glyphSet())) + items.append(ast.GlyphClass(item)) else: raise NotImplementedError(item) return items - def _enum(self, enum): - return ast.GlyphClass(self._coverage(enum.enum, flatten=True)) - def _context(self, context): out = [] for item in context: - coverage = self._coverage(item) - if not isinstance(coverage, (tuple, list)): - coverage = [coverage] - out.extend(coverage) + coverage = self._coverage(item, flatten=True) + if len(coverage) > 1: + coverage = ast.GlyphClass(coverage) + else: + coverage = coverage[0] + out.append(coverage) return out def _groupDefinition(self, group): name = self._className(group.name) - glyphs = self._enum(group.enum) - glyphclass = ast.GlyphClassDefinition(name, glyphs) - - self._glyphclasses[group.name.lower()] = glyphclass + glyphs = self._coverage(group.enum.enum, flatten=True) + glyphclass = ast.GlyphClass(glyphs) + classdef = ast.GlyphClassDefinition(name, glyphclass) + self._glyphclasses[group.name.lower()] = classdef def _glyphDefinition(self, glyph): try: @@ -539,7 +546,7 @@ def _gposContextLookup( elif isinstance(pos, VAst.PositionAdjustSingleDefinition): glyphs = [ast.GlyphClass()] for a, b in pos.adjust_single: - glyph = self._coverage(a) + glyph = self._coverage(a, flatten=True) glyphs[0].extend(glyph) if ignore: @@ -552,7 +559,7 @@ def _gposContextLookup( elif isinstance(pos, VAst.PositionAttachDefinition): glyphs = [ast.GlyphClass()] for coverage, _ in pos.coverage_to: - glyphs[0].extend(self._coverage(coverage)) + glyphs[0].extend(self._coverage(coverage, flatten=True)) if ignore: statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index f67424ffbe..8d82cb5866 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -1550,6 +1550,60 @@ def test_anchor_adjust_device(self): 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_target { + 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_target; + + 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_target space; + } lookup; + """ + ), + fea, + ) + def test_use_extension(self): fea = self.parse( 'DEF_LOOKUP "kern1" PROCESS_BASE PROCESS_MARKS ALL ' From 95c081fc1d38d5f8ae0d829da8b402b738705aea Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 22 Apr 2025 13:12:21 +0200 Subject: [PATCH 084/105] [voltToFea] Minor Rename variables to be less cryptic. --- Lib/fontTools/voltLib/voltToFea.py | 44 ++++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index bf9b937d0b..e2403a43b2 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -186,30 +186,38 @@ def _buildFeatureFile(self, tables): # Prune features features = self._features.copy() - for ftag in features: - scripts = features[ftag] - for stag in scripts: - langs = scripts[stag] - for ltag in langs: - langs[ltag] = [l for l in langs[ltag] if l.lower() in self._lookups] - scripts[stag] = {t: l for t, l in langs.items() if l} - features[ftag] = {t: s for t, s in scripts.items() if s} + for feature_tag in features: + scripts = features[feature_tag] + for script_tag in scripts: + langs = scripts[script_tag] + for language_tag in langs: + langs[language_tag] = [ + l for l in langs[language_tag] if l.lower() in self._lookups + ] + scripts[script_tag] = {t: l for t, l in langs.items() if l} + features[feature_tag] = {t: s for t, s in scripts.items() if s} features = {t: f for t, f in features.items() if f} if features: statements.append(ast.Comment("# Features")) - for ftag, scripts in features.items(): - feature = ast.FeatureBlock(ftag) - stags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1) - for stag in stags: - feature.statements.append(ast.ScriptStatement(stag)) - ltags = sorted(scripts[stag], key=lambda k: 0 if k == "dflt" else 1) - for ltag in ltags: - include_default = True if ltag == "dflt" else False + for feature_tag, scripts in features.items(): + feature = ast.FeatureBlock(feature_tag) + script_tags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1) + for script_tag in script_tags: + feature.statements.append(ast.ScriptStatement(script_tag)) + language_tags = sorted( + scripts[script_tag], + key=lambda k: 0 if k == "dflt" else 1, + ) + for language_tag in language_tags: + include_default = True if language_tag == "dflt" else False feature.statements.append( - ast.LanguageStatement(ltag, include_default=include_default) + ast.LanguageStatement( + language_tag, + include_default=include_default, + ) ) - for name in scripts[stag][ltag]: + for name in scripts[script_tag][language_tag]: lookup = self._lookups[name.lower()] lookupref = ast.LookupReferenceStatement(lookup) feature.statements.append(lookupref) From 02a1c298d0b0d69720c7e2b9f0f4409261e0d925 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 22 Apr 2025 13:25:00 +0200 Subject: [PATCH 085/105] [voltToFea] Handle aalt feature differently In FEA syntax aalt is not allowed to have script or language statements, but in OpenType and VOLT it is a regular feature like any other. We now emit a warning and use only lookups from first script and language. --- Lib/fontTools/voltLib/voltToFea.py | 28 +++++++--- Tests/voltLib/volttofea_test.py | 85 ++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index e2403a43b2..8ea158c52d 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -203,20 +203,34 @@ def _buildFeatureFile(self, tables): for feature_tag, scripts in features.items(): feature = ast.FeatureBlock(feature_tag) script_tags = sorted(scripts, key=lambda k: 0 if k == "DFLT" else 1) + if feature_tag == "aalt" and len(script_tags) > 1: + log.warning( + "FEA syntax does not allow script statements in 'aalt' feature, " + "so only lookups from the first script will be included." + ) + script_tags = script_tags[:1] for script_tag in script_tags: - feature.statements.append(ast.ScriptStatement(script_tag)) + if feature_tag != "aalt": + feature.statements.append(ast.ScriptStatement(script_tag)) language_tags = sorted( scripts[script_tag], key=lambda k: 0 if k == "dflt" else 1, ) + if feature_tag == "aalt" and len(language_tags) > 1: + log.warning( + "FEA syntax does not allow language statements in 'aalt' feature, " + "so only lookups from the first language will be included." + ) + language_tags = language_tags[:1] for language_tag in language_tags: - include_default = True if language_tag == "dflt" else False - feature.statements.append( - ast.LanguageStatement( - language_tag, - include_default=include_default, + if feature_tag != "aalt": + include_default = True if language_tag == "dflt" else False + feature.statements.append( + ast.LanguageStatement( + language_tag, + include_default=include_default, + ) ) - ) for name in scripts[script_tag][language_tag]: lookup = self._lookups[name.lower()] lookupref = ast.LookupReferenceStatement(lookup) diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 8d82cb5866..d0f84bc271 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -1685,6 +1685,91 @@ def test_group_nested_enum(self): ) 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): vtp = DATADIR / "Nutso.vtp" fea = DATADIR / "Nutso.fea" From 23557088d3a321b3af940ae3e416d3302d22b0cc Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Thu, 24 Apr 2025 14:50:28 +0200 Subject: [PATCH 086/105] [voltToFea] Add useExtension to all lookups not just GPOS ones --- Lib/fontTools/voltLib/voltToFea.py | 22 +++++++++++++++++----- Tests/voltLib/volttofea_test.py | 12 ++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 8ea158c52d..9dfbee3fe5 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -715,13 +715,21 @@ def _lookupDefinition(self, lookup): lookupflags = ast.LookupFlagStatement( flags, mark_attachement, mark_filtering ) + + use_extension = False + if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"): + use_extension = True + if "\\" in lookup.name: # Merge sub lookups as subtables (lookups named “base\sub”), # makeotf/feaLib will issue a warning and ignore the subtable # statement if it is not a pairpos lookup, though. name = lookup.name.split("\\")[0] if name.lower() not in self._lookups: - fealookup = ast.LookupBlock(self._lookupName(name)) + fealookup = ast.LookupBlock( + self._lookupName(name), + use_extension=use_extension, + ) if lookupflags is not None: fealookup.statements.append(lookupflags) fealookup.statements.append(ast.Comment("# " + lookup.name)) @@ -731,7 +739,10 @@ def _lookupDefinition(self, lookup): fealookup.statements.append(ast.Comment("# " + lookup.name)) self._lookups[name.lower()] = fealookup else: - fealookup = ast.LookupBlock(self._lookupName(lookup.name)) + fealookup = ast.LookupBlock( + self._lookupName(lookup.name), + use_extension=use_extension, + ) if lookupflags is not None: fealookup.statements.append(lookupflags) self._lookups[lookup.name.lower()] = fealookup @@ -759,12 +770,13 @@ def _lookupDefinition(self, lookup): self._gsubLookup(lookup, prefix, suffix, ignore, chain, fealookup) if lookup.pos is not None: - if self._settings.get("COMPILER_USEEXTENSIONLOOKUPS"): - fealookup.use_extension = True if prefix or suffix or chain or ignore: if not ignore and targetlookup is None: targetname = self._lookupName(lookup.name + " target") - targetlookup = ast.LookupBlock(targetname) + targetlookup = ast.LookupBlock( + targetname, + use_extension=use_extension, + ) fealookup.targets = getattr(fealookup, "targets", []) fealookup.targets.append(targetlookup) self._gposLookup(lookup, targetlookup) diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index d0f84bc271..0544eea01e 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -1606,6 +1606,14 @@ def test_nested_enum(self): 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" @@ -1622,6 +1630,10 @@ def test_use_extension(self): ) self.assertEqual( "\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" From 8706ed400d85abd94e8902c90477946bdd6a73d6 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 27 Apr 2025 01:00:14 +0300 Subject: [PATCH 087/105] [voltToFea] Cleanup how chained lookups are created No functional change. Prepares for next commit. --- Lib/fontTools/voltLib/voltToFea.py | 100 ++++++++++++++--------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 9dfbee3fe5..a5934eab19 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -87,6 +87,12 @@ def sort_groups(groups): return [group_map[name] for name in sorter.static_order()] +class Lookup(ast.LookupBlock): + def __init__(self, name, use_extension=False, location=None): + super().__init__(name, use_extension, location) + self.chained = [] + + class VoltToFea: _NOT_LOOKUP_NAME_RE = re.compile(r"[^A-Za-z_0-9.]") _NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]") @@ -181,7 +187,7 @@ def _buildFeatureFile(self, tables): if self._lookups: statements.append(ast.Comment("\n# Lookups")) for lookup in self._lookups.values(): - statements.extend(getattr(lookup, "targets", [])) + statements.extend(lookup.chained) statements.append(lookup) # Prune features @@ -541,9 +547,7 @@ def _gposLookup(self, lookup, fealookup): else: raise NotImplementedError(pos) - def _gposContextLookup( - self, lookup, prefix, suffix, ignore, fealookup, targetlookup - ): + def _gposContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained): statements = fealookup.statements assert not lookup.reversal @@ -560,22 +564,20 @@ def _gposContextLookup( if ignore: statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) else: - lookups = (targetlookup, targetlookup) statement = ast.ChainContextPosStatement( - prefix, glyphs, suffix, lookups + prefix, glyphs, suffix, [chained, chained] ) statements.append(statement) elif isinstance(pos, VAst.PositionAdjustSingleDefinition): glyphs = [ast.GlyphClass()] - for a, b in pos.adjust_single: - glyph = self._coverage(a, flatten=True) - glyphs[0].extend(glyph) + for a, _ in pos.adjust_single: + glyphs[0].extend(self._coverage(a, flatten=True)) if ignore: statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) else: statement = ast.ChainContextPosStatement( - prefix, glyphs, suffix, [targetlookup] + prefix, glyphs, suffix, [chained] ) statements.append(statement) elif isinstance(pos, VAst.PositionAttachDefinition): @@ -587,13 +589,13 @@ def _gposContextLookup( statement = ast.IgnorePosStatement([(prefix, glyphs, suffix)]) else: statement = ast.ChainContextPosStatement( - prefix, glyphs, suffix, [targetlookup] + prefix, glyphs, suffix, [chained] ) statements.append(statement) else: raise NotImplementedError(pos) - def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): + def _gsubLookup(self, lookup, prefix, suffix, ignore, forceChain, fealookup): statements = fealookup.statements sub = lookup.sub @@ -616,7 +618,7 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): for glyph, replacements in alternates.items(): statement = ast.AlternateSubstStatement( - prefix, glyph, suffix, ast.GlyphClass(replacements), chain + prefix, glyph, suffix, ast.GlyphClass(replacements), forceChain ) statements.append(statement) return @@ -636,7 +638,7 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): assert len(glyphs) == 1 assert len(replacements) == 1 statement = ast.SingleSubstStatement( - glyphs, replacements, prefix, suffix, chain + glyphs, replacements, prefix, suffix, forceChain ) elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition): assert len(glyphs) == 1 @@ -647,12 +649,12 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): elif isinstance(sub, VAst.SubstitutionMultipleDefinition): assert len(glyphs) == 1 statement = ast.MultipleSubstStatement( - prefix, glyphs[0], suffix, replacements, chain + prefix, glyphs[0], suffix, replacements, forceChain ) elif isinstance(sub, VAst.SubstitutionLigatureDefinition): assert len(replacements) == 1 statement = ast.LigatureSubstStatement( - prefix, glyphs, suffix, replacements[0], chain + prefix, glyphs, suffix, replacements[0], forceChain ) # If any of the input glyphs is a group, we need to @@ -683,7 +685,7 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, chain, fealookup): zipped = [self._glyphName(x) for x in zipped] statements.append( ast.LigatureSubstStatement( - prefix, zipped[:-1], suffix, zipped[-1], chain + prefix, zipped[:-1], suffix, zipped[-1], forceChain ) ) continue @@ -726,7 +728,7 @@ def _lookupDefinition(self, lookup): # statement if it is not a pairpos lookup, though. name = lookup.name.split("\\")[0] if name.lower() not in self._lookups: - fealookup = ast.LookupBlock( + fealookup = Lookup( self._lookupName(name), use_extension=use_extension, ) @@ -739,7 +741,7 @@ def _lookupDefinition(self, lookup): fealookup.statements.append(ast.Comment("# " + lookup.name)) self._lookups[name.lower()] = fealookup else: - fealookup = ast.LookupBlock( + fealookup = Lookup( self._lookupName(lookup.name), use_extension=use_extension, ) @@ -751,40 +753,38 @@ def _lookupDefinition(self, lookup): fealookup.statements.append(ast.Comment("# " + lookup.comments)) contexts = [] - if lookup.context: - for context in lookup.context: - prefix = self._context(context.left) - suffix = self._context(context.right) - ignore = context.ex_or_in == "EXCEPT_CONTEXT" - contexts.append([prefix, suffix, ignore, False]) - # It seems that VOLT will create contextual substitution using - # only the input if there is no other contexts in this lookup. - if ignore and len(lookup.context) == 1: - contexts.append([[], [], False, True]) - else: - contexts.append([[], [], False, False]) - - targetlookup = None - for prefix, suffix, ignore, chain in contexts: - if lookup.sub is not None: - self._gsubLookup(lookup, prefix, suffix, ignore, chain, fealookup) - + for context in lookup.context: + prefix = self._context(context.left) + suffix = self._context(context.right) + ignore = context.ex_or_in == "EXCEPT_CONTEXT" + contexts.append([prefix, suffix, ignore, False]) + # It seems that VOLT will create contextual substitution using + # only the input if there is no other contexts in this lookup. + if ignore and len(lookup.context) == 1: + contexts.append([[], [], False, True]) + + if contexts: if lookup.pos is not None: - if prefix or suffix or chain or ignore: - if not ignore and targetlookup is None: - targetname = self._lookupName(lookup.name + " target") - targetlookup = ast.LookupBlock( - targetname, - use_extension=use_extension, - ) - fealookup.targets = getattr(fealookup, "targets", []) - fealookup.targets.append(targetlookup) - self._gposLookup(lookup, targetlookup) + chained = ast.LookupBlock( + self._lookupName(lookup.name + " target"), + use_extension=use_extension, + ) + fealookup.chained.append(chained) + self._gposLookup(lookup, chained) + for prefix, suffix, ignore, forceChain in contexts: + if lookup.sub is not None: + self._gsubLookup( + lookup, prefix, suffix, ignore, forceChain, fealookup + ) + elif lookup.pos is not None: self._gposContextLookup( - lookup, prefix, suffix, ignore, fealookup, targetlookup + lookup, prefix, suffix, ignore, fealookup, chained ) - else: - self._gposLookup(lookup, fealookup) + else: + if lookup.sub is not None: + self._gsubLookup(lookup, [], [], False, False, fealookup) + elif lookup.pos is not None: + self._gposLookup(lookup, fealookup) def main(args=None): From aa36d59d1e6428e9ae2ee891948d628d7be7c547 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 27 Apr 2025 14:42:36 +0300 Subject: [PATCH 088/105] [votlToFea] Re-do contextual lookups Using inline syntax results in very inefficient lookups, with each statement going into a subtable. Some fonts get too big lookups and even fail to compile. Using explicit lookup blocks for the chained lookups fixes most of such undesired subtable breaks. --- Lib/fontTools/voltLib/voltToFea.py | 115 +++++++++++++------ Tests/voltLib/data/NamdhinggoSIL1006.fea | 12 +- Tests/voltLib/data/Nutso.fea | 138 ++++++++++++----------- Tests/voltLib/volttofea_test.py | 112 +++++++++++------- 4 files changed, 234 insertions(+), 143 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index a5934eab19..a55309edfb 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -595,7 +595,7 @@ def _gposContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained) else: raise NotImplementedError(pos) - def _gsubLookup(self, lookup, prefix, suffix, ignore, forceChain, fealookup): + def _gsubLookup(self, lookup, fealookup): statements = fealookup.statements sub = lookup.sub @@ -606,6 +606,10 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, forceChain, fealookup): if isinstance(sub, VAst.SubstitutionAlternateDefinition): alternates = {} for key, val in sub.mapping.items(): + if not key or not val: + path, line, column = sub.location + log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") + continue glyphs = self._coverage(key) replacements = self._coverage(val) assert len(glyphs) == 1 @@ -618,7 +622,7 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, forceChain, fealookup): for glyph, replacements in alternates.items(): statement = ast.AlternateSubstStatement( - prefix, glyph, suffix, ast.GlyphClass(replacements), forceChain + [], glyph, [], ast.GlyphClass(replacements) ) statements.append(statement) return @@ -628,33 +632,26 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, forceChain, fealookup): path, line, column = sub.location log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") continue - statement = None glyphs = self._coverage(key) replacements = self._coverage(val) - if ignore: - chain_context = (prefix, glyphs, suffix) - statement = ast.IgnoreSubstStatement([chain_context]) - elif isinstance(sub, VAst.SubstitutionSingleDefinition): + if isinstance(sub, VAst.SubstitutionSingleDefinition): assert len(glyphs) == 1 assert len(replacements) == 1 - statement = ast.SingleSubstStatement( - glyphs, replacements, prefix, suffix, forceChain + statements.append( + ast.SingleSubstStatement(glyphs, replacements, [], [], False) ) elif isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition): - assert len(glyphs) == 1 - assert len(replacements) == 1 - statement = ast.ReverseChainSingleSubstStatement( - prefix, suffix, glyphs, replacements - ) + # This is handled in gsubContextLookup() + pass elif isinstance(sub, VAst.SubstitutionMultipleDefinition): assert len(glyphs) == 1 - statement = ast.MultipleSubstStatement( - prefix, glyphs[0], suffix, replacements, forceChain + statements.append( + ast.MultipleSubstStatement([], glyphs[0], [], replacements) ) elif isinstance(sub, VAst.SubstitutionLigatureDefinition): assert len(replacements) == 1 statement = ast.LigatureSubstStatement( - prefix, glyphs, suffix, replacements[0], forceChain + [], glyphs, [], replacements[0], False ) # If any of the input glyphs is a group, we need to @@ -685,13 +682,65 @@ def _gsubLookup(self, lookup, prefix, suffix, ignore, forceChain, fealookup): zipped = [self._glyphName(x) for x in zipped] statements.append( ast.LigatureSubstStatement( - prefix, zipped[:-1], suffix, zipped[-1], forceChain + [], zipped[:-1], [], zipped[-1], False ) ) - continue + else: + statements.append(statement) else: raise NotImplementedError(sub) - statements.append(statement) + + def _gsubContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained): + statements = fealookup.statements + + sub = lookup.sub + + if isinstance(sub, VAst.SubstitutionReverseChainingSingleDefinition): + # Reverse substitutions is a special case, it can’t use chained lookups. + for key, val in sub.mapping.items(): + if not key or not val: + path, line, column = sub.location + log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") + continue + glyphs = self._coverage(key) + replacements = self._coverage(val) + statements.append( + ast.ReverseChainSingleSubstStatement( + prefix, suffix, glyphs, replacements + ) + ) + fealookup.chained = [] + return + + assert not lookup.reversal + + if not isinstance( + sub, + ( + VAst.SubstitutionSingleDefinition, + VAst.SubstitutionMultipleDefinition, + VAst.SubstitutionLigatureDefinition, + VAst.SubstitutionAlternateDefinition, + ), + ): + raise NotImplementedError(type(sub)) + + glyphs = [] + for key, val in sub.mapping.items(): + if not key or not val: + path, line, column = sub.location + log.warning(f"{path}:{line}:{column}: Ignoring empty substitution") + continue + glyphs.extend(self._coverage(key, flatten=True)) + + if len(glyphs) > 1: + glyphs = [ast.GlyphClass(glyphs)] + if ignore: + statements.append(ast.IgnoreSubstStatement([(prefix, glyphs, suffix)])) + else: + statements.append( + ast.ChainContextSubstStatement(prefix, glyphs, suffix, [chained]) + ) def _lookupDefinition(self, lookup): mark_attachement = None @@ -757,24 +806,26 @@ def _lookupDefinition(self, lookup): prefix = self._context(context.left) suffix = self._context(context.right) ignore = context.ex_or_in == "EXCEPT_CONTEXT" - contexts.append([prefix, suffix, ignore, False]) + contexts.append([prefix, suffix, ignore]) # It seems that VOLT will create contextual substitution using # only the input if there is no other contexts in this lookup. if ignore and len(lookup.context) == 1: - contexts.append([[], [], False, True]) + contexts.append([[], [], False]) if contexts: - if lookup.pos is not None: - chained = ast.LookupBlock( - self._lookupName(lookup.name + " target"), - use_extension=use_extension, - ) - fealookup.chained.append(chained) + chained = ast.LookupBlock( + self._lookupName(lookup.name + " chained"), + use_extension=use_extension, + ) + fealookup.chained.append(chained) + if lookup.sub is not None: + self._gsubLookup(lookup, chained) + elif lookup.pos is not None: self._gposLookup(lookup, chained) - for prefix, suffix, ignore, forceChain in contexts: + for prefix, suffix, ignore in contexts: if lookup.sub is not None: - self._gsubLookup( - lookup, prefix, suffix, ignore, forceChain, fealookup + self._gsubContextLookup( + lookup, prefix, suffix, ignore, fealookup, chained ) elif lookup.pos is not None: self._gposContextLookup( @@ -782,7 +833,7 @@ def _lookupDefinition(self, lookup): ) else: if lookup.sub is not None: - self._gsubLookup(lookup, [], [], False, False, fealookup) + self._gsubLookup(lookup, fealookup) elif lookup.pos is not None: self._gposLookup(lookup, fealookup) diff --git a/Tests/voltLib/data/NamdhinggoSIL1006.fea b/Tests/voltLib/data/NamdhinggoSIL1006.fea index 0aa5ddd357..575907316f 100644 --- a/Tests/voltLib/data/NamdhinggoSIL1006.fea +++ b/Tests/voltLib/data/NamdhinggoSIL1006.fea @@ -149,21 +149,21 @@ 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 { diff --git a/Tests/voltLib/data/Nutso.fea b/Tests/voltLib/data/Nutso.fea index 3e255d7957..bd5339e0e7 100644 --- a/Tests/voltLib/data/Nutso.fea +++ b/Tests/voltLib/data/Nutso.fea @@ -111,8 +111,12 @@ 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 { @@ -140,9 +144,13 @@ lookup frac.noslash { 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 { @@ -150,114 +158,114 @@ 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; -} fracmark.init_1.10_target; +} 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; -} fracmark.init_2.10_target; +} 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; -} fracmark.init_3.10_target; +} 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; -} fracmark.init_4.10_target; +} 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; -} fracmark.init_5.10_target; +} 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; -} fracmark.init_6.10_target; +} 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; -} fracmark.init_7.10_target; +} 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; -} fracmark.init_8.10_target; +} 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; -} fracmark.init_9.10_target; +} 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 { diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 0544eea01e..57c0a11bd1 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -445,14 +445,22 @@ def test_substitution_single_in_context(self): "END_SUBSTITUTION" ) self.assertEqual( - "# 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", + 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, ) @@ -477,14 +485,22 @@ def test_substitution_single_in_contexts(self): "END_SUBSTITUTION" ) self.assertEqual( - "# 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", + 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, ) @@ -509,14 +525,22 @@ def test_substitution_single_except_context(self): "END_SUBSTITUTION" ) self.assertEqual( - "# 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", + 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, ) @@ -740,10 +764,18 @@ def test_substitution_no_reversal(self): "END_SUBSTITUTION" ) self.assertEqual( - "\n# Lookups\n" - "lookup Lookup {\n" - " sub a' [a b] by a.alt;\n" - "} Lookup;\n", + dedent( + """ + # Lookups + lookup Lookup_chained { + sub a by a.alt; + } Lookup_chained; + + lookup Lookup { + sub a' lookup Lookup_chained [a b]; + } Lookup; + """ + ), fea, ) @@ -1082,15 +1114,15 @@ def test_position_attach_in_context(self): "markClass gravecomb @top.test;\n" "\n" "# Lookups\n" - "lookup test_target {\n" + "lookup test_chained {\n" " pos base a\n" " mark @top.test;\n" - "} test_target;\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, ) @@ -1239,13 +1271,13 @@ def test_position_adjust_pair_in_context(self): ) self.assertEqual( "\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, ) @@ -1289,14 +1321,14 @@ def test_position_adjust_single_in_context(self): ) self.assertEqual( "\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, ) @@ -1588,16 +1620,16 @@ def test_nested_enum(self): dedent( """ # Lookups - lookup lookup_target { + 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_target; + } 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_target space; + pos [a b c d e f g h i j k l m n o p q]' lookup lookup_chained space; } lookup; """ ), From 4efb1a42e0329c7601f4b3358950f496353bc403 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 27 Apr 2025 16:59:28 +0300 Subject: [PATCH 089/105] [voltToFea] make sure language tags are four chars VOLT might skip the padding spaces, but feaLib.ast will assert if not equal to 4. --- Lib/fontTools/voltLib/voltToFea.py | 2 +- Tests/voltLib/volttofea_test.py | 39 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index a55309edfb..ad73fceeb5 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -233,7 +233,7 @@ def _buildFeatureFile(self, tables): include_default = True if language_tag == "dflt" else False feature.statements.append( ast.LanguageStatement( - language_tag, + language_tag.ljust(4), include_default=include_default, ) ) diff --git a/Tests/voltLib/volttofea_test.py b/Tests/voltLib/volttofea_test.py index 57c0a11bd1..60cb5badcf 100644 --- a/Tests/voltLib/volttofea_test.py +++ b/Tests/voltLib/volttofea_test.py @@ -291,6 +291,45 @@ def test_langsys_no_lang_name(self): ) 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( 'DEF_SCRIPT NAME "Latin" TAG "latn"\n' From 7761bc6a1d224a1c467742b129d072ee6006202a Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sun, 27 Apr 2025 20:59:10 +0300 Subject: [PATCH 090/105] [voltLib] Ignore invalid uses of REVERSAL flag VOLT ignores it unless it is a single substitution lookup, so we now do the same instead of raising. --- Lib/fontTools/voltLib/parser.py | 24 +++++++-------- Lib/fontTools/voltLib/voltToFea.py | 4 --- Tests/voltLib/parser_test.py | 48 +++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 30 deletions(-) diff --git a/Lib/fontTools/voltLib/parser.py b/Lib/fontTools/voltLib/parser.py index fd2aed65be..31a76e69bf 100644 --- a/Lib/fontTools/voltLib/parser.py +++ b/Lib/fontTools/voltLib/parser.py @@ -313,23 +313,23 @@ def parse_substitution_(self, reversal): self.expect_keyword_("END_SUBSTITUTION") max_src = max([len(cov) for cov in src]) max_dest = max([len(cov) for cov in dest]) + # many to many or mixed is invalid - if (max_src > 1 and max_dest > 1) or ( - reversal and (max_src > 1 or max_dest > 1) - ): + if max_src > 1 and max_dest > 1: raise VoltLibError("Invalid substitution type", location) + mapping = dict(zip(tuple(src), tuple(dest))) if max_src == 1 and max_dest == 1: - if reversal: - sub = ast.SubstitutionReverseChainingSingleDefinition( - mapping, location=location - ) + # Alternate substitutions are represented by adding multiple + # substitutions for the same glyph, so we detect that here + glyphs = [x.glyphSet() for cov in src for x in cov] # flatten src + if len(set(glyphs)) != len(glyphs): # src has duplicates + sub = ast.SubstitutionAlternateDefinition(mapping, location=location) else: - # Alternate substitutions are represented by adding multiple - # substitutions for the same glyph, so we detect that here - glyphs = [x.glyphSet() for cov in src for x in cov] # flatten src - if len(set(glyphs)) != len(glyphs): # src has duplicates - sub = ast.SubstitutionAlternateDefinition( + if reversal: + # Reversal is valid only for single glyph substitutions + # and VOLT ignores it otherwise. + sub = ast.SubstitutionReverseChainingSingleDefinition( mapping, location=location ) else: diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index ad73fceeb5..1094cf46c3 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -550,8 +550,6 @@ def _gposLookup(self, lookup, fealookup): def _gposContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained): statements = fealookup.statements - assert not lookup.reversal - pos = lookup.pos if isinstance(pos, VAst.PositionAdjustPairDefinition): for (idx1, idx2), (pos1, pos2) in pos.adjust_pair.items(): @@ -712,8 +710,6 @@ def _gsubContextLookup(self, lookup, prefix, suffix, ignore, fealookup, chained) fealookup.chained = [] return - assert not lookup.reversal - if not isinstance( sub, ( 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()] From 9a7d91c75da7db7f1a8e9723cb4dc37a17961b5f Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 15 Mar 2025 21:08:20 +0200 Subject: [PATCH 091/105] [voltToFea] Allow passing VoltFile as input --- Lib/fontTools/voltLib/voltToFea.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 1094cf46c3..12c759dd18 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -98,7 +98,10 @@ class VoltToFea: _NOT_CLASS_NAME_RE = re.compile(r"[^A-Za-z_0-9.\-]") def __init__(self, file_or_path, font=None): - self._file_or_path = file_or_path + if isinstance(file_or_path, VAst.VoltFile): + self._doc, self._file_or_path = file_or_path, None + else: + self._doc, self._file_or_path = None, file_or_path self._font = font self._glyph_map = {} @@ -261,7 +264,9 @@ def _buildFeatureFile(self, tables): return doc def convert(self, tables=None): - doc = VoltParser(self._file_or_path).parse() + if self._doc is None: + self._doc = VoltParser(self._file_or_path).parse() + doc = self._doc if tables is None: tables = TABLES From 361f96bea5225aad29c009199cb5ab06f2ba4f71 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 15 Mar 2025 21:53:07 +0200 Subject: [PATCH 092/105] [voltToFea] Add option to not warn about unsupported settings --- Lib/fontTools/voltLib/voltToFea.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/fontTools/voltLib/voltToFea.py b/Lib/fontTools/voltLib/voltToFea.py index 12c759dd18..d552f4b52a 100644 --- a/Lib/fontTools/voltLib/voltToFea.py +++ b/Lib/fontTools/voltLib/voltToFea.py @@ -139,7 +139,7 @@ def _className(self, name): self._class_names[name] = res return self._class_names[name] - def _collectStatements(self, doc, tables): + def _collectStatements(self, doc, tables, ignore_unsupported_settings=False): # Collect glyph difinitions first, as we need them to map VOLT glyph names to font glyph name. for statement in doc.statements: if isinstance(statement, VAst.GlyphDefinition): @@ -157,7 +157,7 @@ def _collectStatements(self, doc, tables): if "GPOS" in tables: self._anchorDefinition(statement) elif isinstance(statement, VAst.SettingDefinition): - self._settingDefinition(statement) + self._settingDefinition(statement, ignore_unsupported_settings) elif isinstance(statement, (VAst.GlyphDefinition, VAst.GroupDefinition)): pass # Handled above elif isinstance(statement, VAst.ScriptDefinition): @@ -263,7 +263,7 @@ def _buildFeatureFile(self, tables): return doc - def convert(self, tables=None): + def convert(self, tables=None, ignore_unsupported_settings=False): if self._doc is None: self._doc = VoltParser(self._file_or_path).parse() doc = self._doc @@ -273,7 +273,7 @@ def convert(self, tables=None): if self._font is not None: self._glyph_order = self._font.getGlyphOrder() - self._collectStatements(doc, tables) + self._collectStatements(doc, tables, ignore_unsupported_settings) fea = self._buildFeatureFile(tables) return fea.asFea() @@ -368,10 +368,10 @@ def _scriptDefinition(self, script): assert ltag not in self._features[ftag][stag] self._features[ftag][stag][ltag] = lookups.keys() - def _settingDefinition(self, setting): + def _settingDefinition(self, setting, ignore_unsupported=False): if setting.name.startswith("COMPILER_"): self._settings[setting.name] = setting.value - else: + elif not ignore_unsupported: log.warning(f"Unsupported setting ignored: {setting.name}") def _adjustment(self, adjustment): From 322806a8dc32c65324258b4f1e08661b5559aa4e Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Tue, 29 Apr 2025 02:12:48 +0300 Subject: [PATCH 093/105] [voltLib] Add preliminary support for compiling fonts Add __main__.py CLI that takes a MS VOLT project and compiles it to font tables. Layout tables are currently converted first to feature files and compiled with feaLib. Support for other tables (e.g. cmap) will be added later. At some point direct compilation of layout tables (without converting to feature files first) might be added. --- Lib/fontTools/voltLib/__main__.py | 206 ++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 Lib/fontTools/voltLib/__main__.py diff --git a/Lib/fontTools/voltLib/__main__.py b/Lib/fontTools/voltLib/__main__.py new file mode 100644 index 0000000000..aa2c3b3e61 --- /dev/null +++ b/Lib/fontTools/voltLib/__main__.py @@ -0,0 +1,206 @@ +import argparse +import logging +import sys +from io import StringIO +from pathlib import Path + +from fontTools import configLogger +from fontTools.feaLib.builder import addOpenTypeFeaturesFromString +from fontTools.feaLib.error import FeatureLibError +from fontTools.feaLib.lexer import Lexer +from fontTools.misc.cliTools import makeOutputFileName +from fontTools.ttLib import TTFont, TTLibError +from fontTools.voltLib.parser import Parser +from fontTools.voltLib.voltToFea import TABLES, VoltToFea + +log = logging.getLogger("fontTools.feaLib") + +SUPPORTED_TABLES = TABLES + ["cmap"] + + +def invalid_fea_glyph_name(name): + """Check if the glyph name is valid according to FEA syntax.""" + if name[0] not in Lexer.CHAR_NAME_START_: + return True + if any(c not in Lexer.CHAR_NAME_CONTINUATION_ for c in name[1:]): + return True + return False + + +def sanitize_glyph_name(name): + """Sanitize the glyph name to ensure it is valid according to FEA syntax.""" + sanitized = "" + for i, c in enumerate(name): + if i == 0 and c not in Lexer.CHAR_NAME_START_: + sanitized += "a" + c + elif c not in Lexer.CHAR_NAME_CONTINUATION_: + sanitized += "_" + else: + sanitized += c + + return sanitized + + +def main(args=None): + """Build tables from a MS VOLT project into an OTF font""" + parser = argparse.ArgumentParser( + description="Use fontTools to compile MS VOLT projects." + ) + parser.add_argument( + "input", + metavar="INPUT", + help="Path to the input font/VTP file to process", + type=Path, + ) + parser.add_argument( + "-f", + "--font", + metavar="INPUT_FONT", + help="Path to the input font (if INPUT is a VTP file)", + type=Path, + ) + parser.add_argument( + "-o", + "--output", + dest="output", + metavar="OUTPUT", + help="Path to the output font.", + type=Path, + ) + parser.add_argument( + "-t", + "--tables", + metavar="TABLE_TAG", + choices=SUPPORTED_TABLES, + nargs="+", + help="Specify the table(s) to be built.", + ) + parser.add_argument( + "-F", + "--debug-feature-file", + help="Write the generated feature file to disk.", + action="store_true", + ) + parser.add_argument( + "--ship", + help="Remove source VOLT tables from output font.", + action="store_true", + ) + parser.add_argument( + "-v", + "--verbose", + help="Increase the logger verbosity. Multiple -v options are allowed.", + action="count", + default=0, + ) + parser.add_argument( + "-T", + "--traceback", + help="show traceback for exceptions.", + action="store_true", + ) + options = parser.parse_args(args) + + levels = ["WARNING", "INFO", "DEBUG"] + configLogger(level=levels[min(len(levels) - 1, options.verbose)]) + + output_font = options.output or Path( + makeOutputFileName(options.font or options.input) + ) + log.info(f"Compiling MS VOLT to '{output_font}'") + + file_or_path = options.input + font = None + + # If the input is a font file, extract the VOLT data from the "TSIV" table + try: + font = TTFont(file_or_path) + if "TSIV" in font: + file_or_path = StringIO(font["TSIV"].data.decode("utf-8")) + else: + log.error('"TSIV" table is missing') + return 1 + except TTLibError: + pass + + # If input is not a font file, the font must be provided + if font is None: + if not options.font: + log.error("Please provide an input font") + return 1 + font = TTFont(options.font) + + # FEA syntax does not allow some glyph names that VOLT accepts, so if we + # found such glyph name we will temporarily rename such glyphs. + glyphOrder = font.getGlyphOrder() + tempGlyphOrder = None + if any(invalid_fea_glyph_name(n) for n in glyphOrder): + tempGlyphOrder = [] + for n in glyphOrder: + if invalid_fea_glyph_name(n): + n = sanitize_glyph_name(n) + existing = set(tempGlyphOrder) | set(glyphOrder) + while n in existing: + n = "a" + n + tempGlyphOrder.append(n) + font.setGlyphOrder(tempGlyphOrder) + + doc = Parser(file_or_path).parse() + + log.info("Converting VTP data to FEA") + converter = VoltToFea(doc, font) + try: + fea = converter.convert(options.tables, ignore_unsupported_settings=True) + except NotImplementedError as e: + if options.traceback: + raise + location = getattr(e.args[0], "location", None) + message = f'"{e}" is not supported' + if location: + path, line, column = location + log.error(f"{path}:{line}:{column}: {message}") + else: + log.error(message) + return 1 + + fea_filename = options.input + if options.debug_feature_file: + fea_filename = output_font.with_suffix(".fea") + log.info(f"Writing FEA to '{fea_filename}'") + with open(fea_filename, "w") as fp: + fp.write(fea) + + log.info("Compiling FEA to OpenType tables") + try: + addOpenTypeFeaturesFromString( + font, + fea, + filename=fea_filename, + tables=options.tables, + ) + except FeatureLibError as e: + if options.traceback: + raise + log.error(e) + return 1 + + if options.ship: + for tag in ["TSIV", "TSIS", "TSIP", "TSID"]: + if tag in font: + del font[tag] + + # Restore original glyph names. + if tempGlyphOrder: + import io + + f = io.BytesIO() + font.save(f) + font = TTFont(f) + font.setGlyphOrder(glyphOrder) + font["post"].extraNames = [] + + font.save(output_font) + + +if __name__ == "__main__": + sys.exit(main()) From bc27b6ba7866eb278ec45616a61c32198ec0b977 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:10:26 +0100 Subject: [PATCH 094/105] Update minimum python versions for tools --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"] From 61cb7f0f4dbfb427cb3142310acc415a8a832574 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:10:37 +0100 Subject: [PATCH 095/105] Update license metadata --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ec79d26dd2..c74aeead87 100755 --- a/setup.py +++ b/setup.py @@ -158,7 +158,6 @@ 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", @@ -502,6 +501,7 @@ 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.9", long_description=long_description, From d13f12f95ba5265e7d5c6ea2bf025ff3afdef783 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:12:13 +0100 Subject: [PATCH 096/105] Use uv to build packages --- .github/workflows/wheels.yml | 11 +++-------- tox.ini | 10 ++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d0fc3c553e..3f018e26b5 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,16 +22,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.x' - - name: Install dependencies - run: | - pip install setuptools wheel twine + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Build source distribution and pure-python wheel run: | - python setup.py sdist bdist_wheel + uv build - uses: actions/upload-artifact@v4 with: name: pure diff --git a/tox.ini b/tox.ini index 256fc873a5..dfa1da6110 100644 --- a/tox.ini +++ b/tox.ini @@ -58,12 +58,14 @@ commands = [testenv:package_readme] description = check that the long description is valid (need for PyPi) -deps = twine >= 1.12.1 - pip >= 18.0.0 +deps = + twine >= 1.12.1 + uv skip_install = true extras = -commands = pip wheel -w {envtmpdir}/build --no-deps . - twine check {envtmpdir}/build/* +commands = + uv build --quiet --wheel --out-dir {envtmpdir}/build + twine check {envtmpdir}/build/* [testenv:bdist] deps = From 3f1dbf6ddcd2e45af34f284f2a36a0b991c25a2d Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:17:52 +0100 Subject: [PATCH 097/105] Remove unused tox commands --- tox.ini | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/tox.ini b/tox.ini index dfa1da6110..b026e4b91b 100644 --- a/tox.ini +++ b/tox.ini @@ -66,32 +66,3 @@ extras = commands = uv build --quiet --wheel --out-dir {envtmpdir}/build twine check {envtmpdir}/build/* - -[testenv:bdist] -deps = - setuptools - wheel -skip_install = true -install_command = - # make sure we use the latest setuptools and wheel - pip install --upgrade {opts} {packages} -whitelist_externals = - rm -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 - -[testenv:pypi] -deps = - {[testenv:bdist]deps} - twine -skip_install = true -passenv = TWINE_USERNAME TWINE_PASSWORD -commands = - {[testenv:bdist]commands} - twine upload dist/*.whl dist/*.zip From c19c471569e084a2e6382d7e1340204a0b5fd2a1 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:18:02 +0100 Subject: [PATCH 098/105] Remove unused Makefile targets --- Makefile | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Makefile b/Makefile index 21cad6cd20..86b3c2baa5 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,4 @@ -all: - ./setup.py build - -dist: - ./setup.py sdist bdist_wheel - -install: - pip install --ignore-installed . - -install-user: - pip install --ignore-installed --user . - -uninstall: - pip uninstall --yes fonttools - -check: all - pytest - -clean: - ./setup.py clean --all - docs: cd Doc && $(MAKE) html -.PHONY: all dist install install-user uninstall check clean docs +.PHONY: docs From 8da59df9d91049ac82560a2ee4e74d80d1af9900 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:27:13 +0100 Subject: [PATCH 099/105] Build docs through tox --- Doc/README.md | 25 ++++--------------------- tox.ini | 8 ++++++++ 2 files changed, 12 insertions(+), 21 deletions(-) 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/tox.ini b/tox.ini index b026e4b91b..935d94b683 100644 --- a/tox.ini +++ b/tox.ini @@ -66,3 +66,11 @@ extras = commands = uv build --quiet --wheel --out-dir {envtmpdir}/build twine check {envtmpdir}/build/* + +[testenv:docs] +description = Build the documentation. +deps = + -r requirements.txt + -r Doc/docs-requirements.txt +commands = + sphinx-build -W -j auto Doc/source Doc/build From 6dcf268c1bcba60da237efdc022350d3adc71187 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 29 Apr 2025 16:27:21 +0100 Subject: [PATCH 100/105] Remove unused Makefile --- Makefile | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 Makefile diff --git a/Makefile b/Makefile deleted file mode 100644 index 86b3c2baa5..0000000000 --- a/Makefile +++ /dev/null @@ -1,4 +0,0 @@ -docs: - cd Doc && $(MAKE) html - -.PHONY: docs From 425263e23b9450bdb67a29be0dc6342f126673d1 Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 6 May 2025 10:28:56 +0100 Subject: [PATCH 101/105] Remove py38 env from tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 935d94b683..b9364c0841 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.0 -envlist = lint, py3{8,9,10,11,12,13}-cov, htmlcov +envlist = lint, py3{9,10,11,12,13}-cov, htmlcov skip_missing_interpreters=true [testenv] From eba55226c83290b5d32db407935eb19c9eeaa78a Mon Sep 17 00:00:00 2001 From: Nikolaus Waxweiler Date: Tue, 6 May 2025 10:32:54 +0100 Subject: [PATCH 102/105] Blacken --- Tests/misc/py23_test.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/misc/py23_test.py b/Tests/misc/py23_test.py index f31a5b8261..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( From cc10bfc7d18e97efe1c5239ce5f47ba73050e3db Mon Sep 17 00:00:00 2001 From: Jens Kutilek Date: Tue, 6 May 2025 14:00:37 +0200 Subject: [PATCH 103/105] Add some PointPen annotations (#3820) * Add more type annotations to pointPens * Remove unused variable * Fixes for Python 3.8 --- Lib/fontTools/pens/pointPen.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) 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) From 54013087e3dd5ea802f0ef6fb3442763314b3965 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 10 May 2025 20:16:59 +0300 Subject: [PATCH 104/105] Update NEWS.rst --- NEWS.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index c16e31e0ae..00b5ea5e9c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,38 @@ +- Drop Python 3.8, require 3.9+ (#3819) +- [HVAR, VVAR] Prune unused regions when using a direct mapping (#3797) +- [Docs] Improvements to ufoLib documentation (#3721) +- [Docs] Improvements to varLib documentation (#3727) +- [Docs] Improvements to Pens and pen-module documentation (#3724) +- [Docs] Miscellany updates to docs (misc modules and smaller modules) (#3730) +- [subset] Close codepoints over BiDi mirror variants. (#3801) +- [feaLib] Fix serializing ChainContextPosStatement and + ChainContextSubstStatement in some rare cases (#3788) +- [designspaceLib] Clarify user expectations for getStatNames (#2892) +- [GVAR] Add support for new `GVAR` table (#3728) +- [TSI0, TSI5] Derive number of entries to decompile from data length (#2477) +- [ttLib] Fix `AttributeError` when reporting table overflow (#3808) +- [ttLib] Apply rounding more often in getCoordinates (#3798) +- [ttLib] Ignore component bounds if empty (#3799) +- [ttLib] Change the separator for duplicate glyph names from "#" to "." (#3809) +- [feaLib] Support subtable breaks in CursivePos, MarkBasePos, MarkToLigPos and + MarkToMarkPos lookups (#3800, #3807) +- [feaLib] If the same lookup has single substitutions and ligature + substitutions, upgrade single substitutions to ligature substitutions with + one input glyph (#3805) +- [feaLib] Correctly handle in single pos lookups (#3803) +- [feaLib] Remove duplicates from class pair pos classes instead of raising an + error (#3804) +- [feaLib] Support creating extension lookups using useExtenion lookup flag + instead of silently ignoring it (#3811) +- [STAT] Add typing for the simpler STAT arguments (#3812) +- [otlLib.builder] Add future import for annotations (#3814) +- [cffLib] Fix reading supplement encoding (#3813) +- [voltLib] Add some missing functionality and fixes to voltLib and VoltToFea, + making the conversion to feature files more robust. Add also `fonttools + voltLib` command line tool to compile VOLT sources directly (doing an + intermediate fea conversion internally) (#3818) +- [pens] Add some PointPen annotations (#3820) + 4.57.0 (released 2025-04-03) ---------------------------- From ac4c5473c1495b129a87d6818b9cb0f1423d3489 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 10 May 2025 20:17:21 +0300 Subject: [PATCH 105/105] Release 4.58.0 --- Lib/fontTools/__init__.py | 2 +- NEWS.rst | 3 +++ setup.cfg | 2 +- setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/fontTools/__init__.py b/Lib/fontTools/__init__.py index c69b6736ee..6e41eb43a8 100644 --- a/Lib/fontTools/__init__.py +++ b/Lib/fontTools/__init__.py @@ -3,6 +3,6 @@ log = logging.getLogger(__name__) -version = __version__ = "4.57.1.dev0" +version = __version__ = "4.58.0" __all__ = ["version", "log", "configLogger"] diff --git a/NEWS.rst b/NEWS.rst index 00b5ea5e9c..a6af57ee70 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,6 @@ +4.58.0 (released 2025-05-10) +---------------------------- + - Drop Python 3.8, require 3.9+ (#3819) - [HVAR, VVAR] Prune unused regions when using a direct mapping (#3797) - [Docs] Improvements to ufoLib documentation (#3721) diff --git a/setup.cfg b/setup.cfg index 249224ae61..c280c349f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.57.1.dev0 +current_version = 4.58.0 commit = True tag = False tag_name = {new_version} diff --git a/setup.py b/setup.py index c74aeead87..1d9f1ac9f9 100755 --- a/setup.py +++ b/setup.py @@ -493,7 +493,7 @@ def build_extensions(self): setup_params = dict( name="fonttools", - version="4.57.1.dev0", + version="4.58.0", description="Tools to manipulate font files", author="Just van Rossum", author_email="just@letterror.com",