Repository: googlefonts/ufo2ft
Pull request #819 (2024-02-13 15:01:35)

outlineCompiler: Support generating MATH table

khaledhosny: I moved the table building code to fontTools.otlLib, so this now depends on https://github.com/fonttools/fonttools/pull/3446.
khaledhosny: Needs some tests, but ready for review otherwise.
khaledhosny: The tests are failing since we need to cut a new FontTools release.
anthrotype: > The tests are failing since we need to cut a new FontTools release.

can you work on that one?
khaledhosny: > > The tests are failing since we need to cut a new FontTools release.
> 
> can you work on that one?

Sure, I’ll do it later tonight.

Patch:
diff --git a/Lib/ufo2ft/constants.py b/Lib/ufo2ft/constants.py
index 696839f..eed6f3a 100644
--- a/Lib/ufo2ft/constants.py
+++ b/Lib/ufo2ft/constants.py
@@ -43,6 +43,11 @@ COLOR_LAYER_MAPPING_KEY = UFO2FT_PREFIX + "colorLayerMapping"
 # ]
 COLR_CLIP_BOXES_KEY = UFO2FT_PREFIX + "colrClipBoxes"
 
+GLYPHS_MATH_PREFIX = "com.nagwa.MATHPlugin."
+GLYPHS_MATH_CONSTANTS_KEY = GLYPHS_MATH_PREFIX + "constants"
+GLYPHS_MATH_VARIANTS_KEY = GLYPHS_MATH_PREFIX + "variants"
+GLYPHS_MATH_EXTENDED_SHAPE_KEY = GLYPHS_MATH_PREFIX + "extendedShape"
+
 OBJECT_LIBS_KEY = "public.objectLibs"
 OPENTYPE_CATEGORIES_KEY = "public.openTypeCategories"
 OPENTYPE_META_KEY = "public.openTypeMeta"
diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py
index 8bd2fff..8ae12f3 100644
--- a/Lib/ufo2ft/outlineCompiler.py
+++ b/Lib/ufo2ft/outlineCompiler.py
@@ -30,6 +30,10 @@ from ufo2ft.constants import (
     COLOR_LAYERS_KEY,
     COLOR_PALETTES_KEY,
     COLR_CLIP_BOXES_KEY,
+    GLYPHS_MATH_CONSTANTS_KEY,
+    GLYPHS_MATH_EXTENDED_SHAPE_KEY,
+    GLYPHS_MATH_PREFIX,
+    GLYPHS_MATH_VARIANTS_KEY,
     OPENTYPE_META_KEY,
     OPENTYPE_POST_UNDERLINE_POSITION_KEY,
     UNICODE_VARIATION_SEQUENCES_KEY,
@@ -94,6 +98,7 @@ class BaseOutlineCompiler:
             "vhea",
             "COLR",
             "CPAL",
+            "MATH",
             "meta",
         ]
     )
@@ -175,6 +180,8 @@ class BaseOutlineCompiler:
             self.setupTable_CPAL()
         if self.meta:
             self.setupTable_meta()
+        if any(key.startswith(GLYPHS_MATH_PREFIX) for key in self.ufo.lib):
+            self.setupTable_MATH()
         self.setupOtherTables()
         if self.colorLayers and self.colrAutoClipBoxes:
             self._computeCOLRClipBoxes()
@@ -1050,6 +1057,181 @@ class BaseOutlineCompiler:
                     f"public.openTypeMeta '{key}' value should be bytes or a string."
                 )
 
+    def _bboxWidth(self, glyph):
+        bbox = self.glyphBoundingBoxes[glyph]
+        if bbox is None:
+            return 0
+        return bbox.xMax - bbox.xMin
+
+    def _bboxHeight(self, glyph):
+        bbox = self.glyphBoundingBoxes[glyph]
+        if bbox is None:
+            return 0
+        return bbox.yMax - bbox.yMin
+
+    def setupTable_MATH(self):
+        """
+        This builds MATH table based on data in the UFO font. The data is stored
+        either in private font/glyph lib keys or as glyph anchors with specific
+        names.
+
+        The data is based on GlyphsApp MATH plugin data as written out by
+        glyphsLib to the UFO font.
+
+        The font lib keys are:
+            - com.nagwa.MATHPlugin.constants: a dictionary of MATH constants as
+              expected by fontTools.otlLib.builder.buildMathTable(). Example:
+
+                ufo.lib["com.nagwa.MATHPlugin.constants"] = {
+                    "ScriptPercentScaleDown": 70,
+                    "ScriptScriptPercentScaleDown": 60,
+                    ...
+                }
+
+            - com.nagwa.MATHPlugin.extendedShape: a list of glyph names that
+              are extended shapes. Example:
+
+                ufo.lib["com.nagwa.MATHPlugin.extendedShapes"] = ["integral", "radical"]
+
+        The glyph lib keys are:
+            - com.nagwa.MATHPlugin.variants: a dictionary of MATH glyph variants
+              keyed by glyph names, and each value is a dictionary with keys
+              "hVariants", "vVariants", "hAssembly", and "vAssembly". Example:
+
+                ufo["braceleft"].lib["com.nagwa.MATHPlugin.variants"] = {
+                    "vVariants": ["braceleft", "braceleft.s1", "braceleft.s2"],
+                    "vAssembly": [
+                        # glyph name, flags, start connector length, end connector length
+                        ["braceleft.bottom", 0, 0, 200],
+                        ["braceleft.extender", 1, 200, 200],
+                        ["braceleft.middle", 0, 100, 100],
+                        ["braceleft.extender", 1, 200, 200],
+                        ["braceleft.top", 0, 200, 0],
+                    ],
+                }
+
+        The anchors are:
+            - math.ic: italic correction anchor
+            - math.ta: top accent attachment anchor
+            - math.tr*: top right kerning anchors
+            - math.tl*: top left kerning anchors
+            - math.br*: bottom right kerning anchors
+            - math.bl*: bottom left kerning anchors
+        """
+        if "MATH" not in self.tables:
+            return
+
+        from fontTools.otlLib.builder import buildMathTable
+
+        ufo = self.ufo
+        constants = ufo.lib.get(GLYPHS_MATH_CONSTANTS_KEY)
+        min_connector_overlap = constants.pop("MinConnectorOverlap", 0)
+
+        italics_correction = {}
+        top_accent_attachment = {}
+        math_kerns = {}
+        kerning_sides = {
+            "tr": "TopRight",
+            "tl": "TopLeft",
+            "br": "BottomRight",
+            "bl": "BottomLeft",
+        }
+        for name, glyph in self.allGlyphs.items():
+            kerns = {}
+            for anchor in glyph.anchors:
+                if anchor.name == "math.ic":
+                    # The anchor x position is absolute, but we want
+                    # a value relative to the glyph's width.
+                    italics_correction[name] = anchor.x - glyph.width
+                if anchor.name == "math.ta":
+                    top_accent_attachment[name] = anchor.x
+                for aName in kerning_sides.keys():
+                    if anchor.name.startswith(f"math.{aName}"):
+                        side = kerning_sides[aName]
+                        # The anchor x positions are absolute, but we want
+                        # values relative to the glyph's width/origin.
+                        x, y = anchor.x, anchor.y
+                        if side.endswith("Right"):
+                            x -= glyph.width
+                        elif side.endswith("Left"):
+                            x = -x
+                        kerns.setdefault(side, []).append([x, y])
+            if kerns:
+                math_kerns[name] = {}
+                # Convert anchor positions to correction heights and kern
+                # values.
+                for side, pts in kerns.items():
+                    pts = sorted(pts, key=lambda pt: pt[1])
+                    # Y positions, the last one is ignored as the last kern
+                    # value is applied to all heights greater than the last one.
+                    correctionHeights = [pt[1] for pt in pts[:-1]]
+                    # X positions
+                    kernValues = [pt[0] for pt in pts]
+                    math_kerns[name][side] = (correctionHeights, kernValues)
+
+        # buildMathTable takes two dictionaries of glyph variants, one for
+        # horizontal variants and one for vertical variants, and items are
+        # tuples of glyph name and the advance width/height of the variant.
+        # Here we convert the UFO data to the expected format and measure the
+        # advances.
+        h_variants = {}
+        v_variants = {}
+        # It also takes two dictionaries of glyph assemblies, one for
+        # horizontal assemblies and one for vertical assemblies, and items are
+        # lists of tuples of assembly parts and italics correction, and the
+        # assembly part includes the advance width/height of the part. Here we
+        # convert the UFO data to the expected format and measure the advances.
+        h_assemblies = {}
+        v_assemblies = {}
+        for name, glyph in self.allGlyphs.items():
+            if GLYPHS_MATH_VARIANTS_KEY in glyph.lib:
+                variants = glyph.lib[GLYPHS_MATH_VARIANTS_KEY]
+                if "hVariants" in variants:
+                    h_variants[name] = [
+                        (n, self._bboxWidth(n)) for n in variants["hVariants"]
+                    ]
+                if "vVariants" in variants:
+                    v_variants[name] = [
+                        (n, self._bboxHeight(n)) for n in variants["vVariants"]
+                    ]
+                if "hAssembly" in variants:
+                    parts = variants["hAssembly"]
+                    h_assemblies[name] = (
+                        [(*part, self._bboxWidth(part[0])) for part in parts],
+                        # If the last part has italic correction, we use it as
+                        # the assembly's.
+                        italics_correction.pop(parts[-1][0], 0),
+                    )
+                if "vAssembly" in variants:
+                    parts = variants["vAssembly"]
+                    v_assemblies[name] = (
+                        [(*part, self._bboxHeight(part[0])) for part in parts],
+                        # If the last part has italic correction, we use it as
+                        # the assembly's.
+                        italics_correction.pop(parts[-1][0], 0),
+                    )
+
+        # Collect the set of extended shapes, and if a shape has vertical
+        # variants, add the variants to the set.
+        extended_shapes = set(ufo.lib.get(GLYPHS_MATH_EXTENDED_SHAPE_KEY, []))
+        for name, variants in v_variants.items():
+            if name in extended_shapes:
+                extended_shapes.update(v[0] for v in variants)
+
+        buildMathTable(
+            self.otf,
+            constants=constants,
+            italicsCorrections=italics_correction,
+            topAccentAttachments=top_accent_attachment,
+            extendedShapes=extended_shapes,
+            mathKerns=math_kerns,
+            minConnectorOverlap=min_connector_overlap,
+            vertGlyphVariants=v_variants,
+            horizGlyphVariants=h_variants,
+            vertGlyphAssembly=v_assemblies,
+            horizGlyphAssembly=h_assemblies,
+        )
+
     def setupOtherTables(self):
         """
         Make the other tables. The default implementation does nothing.
@@ -1652,6 +1834,7 @@ class StubGlyph:
             self.draw = self._drawDefaultNotdef
             self.drawPoints = self._drawDefaultNotdefPoints
         self.reverseContour = reverseContour
+        self.lib = {}
 
     def __len__(self):
         if self.name == ".notdef":
diff --git a/requirements.txt b/requirements.txt
index aed05ff..4ddd37a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-fonttools[ufo,lxml]==4.48.1
+fonttools[ufo,lxml]==4.49.0
 defcon==0.10.3
 compreffor==0.5.5
 booleanOperations==0.9.0
diff --git a/setup.py b/setup.py
index 762d06f..e819092 100644
--- a/setup.py
+++ b/setup.py
@@ -29,7 +29,7 @@ setup(
     setup_requires=pytest_runner + wheel + ["setuptools_scm"],
     tests_require=["pytest>=2.8"],
     install_requires=[
-        "fonttools[ufo]>=4.47.2",
+        "fonttools[ufo]>=4.49.0",
         "cffsubr>=0.3.0",
         "booleanOperations>=0.9.0",
     ],


Test patch:
diff --git a/tests/data/TestMathFont-Regular.ufo/features.fea b/tests/data/TestMathFont-Regular.ufo/features.fea
new file mode 100644
index 0000000..1dafe8f
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/features.fea
@@ -0,0 +1,4 @@
+# Prefix: Languagesystems
+languagesystem DFLT dflt;
+languagesystem math dflt;
+
diff --git a/tests/data/TestMathFont-Regular.ufo/fontinfo.plist b/tests/data/TestMathFont-Regular.ufo/fontinfo.plist
new file mode 100644
index 0000000..b0b9f7a
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/fontinfo.plist
@@ -0,0 +1,46 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>ascender</key>
+    <integer>800</integer>
+    <key>capHeight</key>
+    <integer>656</integer>
+    <key>descender</key>
+    <integer>-200</integer>
+    <key>familyName</key>
+    <string>Test Math Font</string>
+    <key>italicAngle</key>
+    <integer>0</integer>
+    <key>openTypeHeadCreated</key>
+    <string>2024/02/12 18:20:25</string>
+    <key>openTypeOS2Type</key>
+    <array>
+      <integer>3</integer>
+    </array>
+    <key>postscriptBlueValues</key>
+    <array>
+      <integer>-18</integer>
+      <integer>0</integer>
+    </array>
+    <key>postscriptOtherBlues</key>
+    <array>
+      <integer>-201</integer>
+      <integer>-200</integer>
+    </array>
+    <key>postscriptUnderlinePosition</key>
+    <integer>-150</integer>
+    <key>postscriptUnderlineThickness</key>
+    <integer>50</integer>
+    <key>styleName</key>
+    <string>Regular</string>
+    <key>unitsPerEm</key>
+    <integer>1000</integer>
+    <key>versionMajor</key>
+    <integer>1</integer>
+    <key>versionMinor</key>
+    <integer>0</integer>
+    <key>xHeight</key>
+    <integer>449</integer>
+  </dict>
+</plist>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/contents.plist b/tests/data/TestMathFont-Regular.ufo/glyphs/contents.plist
new file mode 100644
index 0000000..455616b
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/contents.plist
@@ -0,0 +1,38 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>parenleft</key>
+    <string>parenleft.glif</string>
+    <key>parenleft.bot</key>
+    <string>parenleft.bot.glif</string>
+    <key>parenleft.ext</key>
+    <string>parenleft.ext.glif</string>
+    <key>parenleft.size1</key>
+    <string>parenleft.size1.glif</string>
+    <key>parenleft.size2</key>
+    <string>parenleft.size2.glif</string>
+    <key>parenleft.size3</key>
+    <string>parenleft.size3.glif</string>
+    <key>parenleft.size4</key>
+    <string>parenleft.size4.glif</string>
+    <key>parenleft.top</key>
+    <string>parenleft.top.glif</string>
+    <key>parenright</key>
+    <string>parenright.glif</string>
+    <key>parenright.bot</key>
+    <string>parenright.bot.glif</string>
+    <key>parenright.ext</key>
+    <string>parenright.ext.glif</string>
+    <key>parenright.size1</key>
+    <string>parenright.size1.glif</string>
+    <key>parenright.size2</key>
+    <string>parenright.size2.glif</string>
+    <key>parenright.size3</key>
+    <string>parenright.size3.glif</string>
+    <key>parenright.size4</key>
+    <string>parenright.size4.glif</string>
+    <key>parenright.top</key>
+    <string>parenright.top.glif</string>
+  </dict>
+</plist>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.bot.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.bot.glif
new file mode 100644
index 0000000..e1c7506
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.bot.glif
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.bot" format="2">
+  <advance width="645"/>
+  <outline>
+    <contour>
+      <point x="595" y="-1129" type="line"/>
+      <point x="291" y="-729"/>
+      <point x="136" y="-184"/>
+      <point x="136" y="373" type="curve" smooth="yes"/>
+      <point x="136" y="687" type="line"/>
+      <point x="90" y="687" type="line"/>
+      <point x="90" y="373" type="line" smooth="yes"/>
+      <point x="90" y="-192"/>
+      <point x="247" y="-749"/>
+      <point x="559" y="-1157" type="curve"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.ext.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.ext.glif
new file mode 100644
index 0000000..2ed6d70
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.ext.glif
@@ -0,0 +1,12 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.ext" format="2">
+  <advance width="645"/>
+  <outline>
+    <contour>
+      <point x="136" y="0" type="line"/>
+      <point x="136" y="800" type="line"/>
+      <point x="90" y="800" type="line"/>
+      <point x="90" y="0" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.glif
new file mode 100644
index 0000000..b754514
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.glif
@@ -0,0 +1,59 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft" format="2">
+  <advance width="331"/>
+  <unicode hex="0028"/>
+  <outline>
+    <contour>
+      <point x="249" y="637" type="line"/>
+      <point x="143" y="529"/>
+      <point x="90" y="381"/>
+      <point x="90" y="232" type="curve" smooth="yes"/>
+      <point x="90" y="83"/>
+      <point x="143" y="-65"/>
+      <point x="249" y="-173" type="curve"/>
+      <point x="281" y="-141" type="line"/>
+      <point x="185" y="-41"/>
+      <point x="136" y="93"/>
+      <point x="136" y="232" type="curve" smooth="yes"/>
+      <point x="136" y="371"/>
+      <point x="185" y="505"/>
+      <point x="281" y="605" type="curve"/>
+    </contour>
+  </outline>
+  <lib>
+    <dict>
+      <key>com.nagwa.MATHPlugin.variants</key>
+      <dict>
+        <key>vAssembly</key>
+        <array>
+          <array>
+            <string>parenleft.bot</string>
+            <integer>0</integer>
+            <integer>0</integer>
+            <integer>314</integer>
+          </array>
+          <array>
+            <string>parenleft.ext</string>
+            <integer>1</integer>
+            <integer>630</integer>
+            <integer>630</integer>
+          </array>
+          <array>
+            <string>parenleft.top</string>
+            <integer>0</integer>
+            <integer>314</integer>
+            <integer>0</integer>
+          </array>
+        </array>
+        <key>vVariants</key>
+        <array>
+          <string>parenleft</string>
+          <string>parenleft.size1</string>
+          <string>parenleft.size2</string>
+          <string>parenleft.size3</string>
+          <string>parenleft.size4</string>
+        </array>
+      </dict>
+    </dict>
+  </lib>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size1.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size1.glif
new file mode 100644
index 0000000..6d0b1d7
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size1.glif
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.size1" format="2">
+  <advance width="416"/>
+  <outline>
+    <contour>
+      <point x="332" y="687" type="line"/>
+      <point x="171" y="521"/>
+      <point x="90" y="294"/>
+      <point x="90" y="65" type="curve" smooth="yes"/>
+      <point x="90" y="-164"/>
+      <point x="171" y="-391"/>
+      <point x="332" y="-557" type="curve"/>
+      <point x="366" y="-525" type="line"/>
+      <point x="213" y="-367"/>
+      <point x="136" y="-154"/>
+      <point x="136" y="65" type="curve" smooth="yes"/>
+      <point x="136" y="284"/>
+      <point x="213" y="497"/>
+      <point x="366" y="655" type="curve"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size2.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size2.glif
new file mode 100644
index 0000000..a40544a
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size2.glif
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.size2" format="2">
+  <advance width="479"/>
+  <outline>
+    <contour>
+      <point x="393" y="685" type="line"/>
+      <point x="192" y="437"/>
+      <point x="90" y="100"/>
+      <point x="90" y="-241" type="curve" smooth="yes"/>
+      <point x="90" y="-582"/>
+      <point x="192" y="-919"/>
+      <point x="393" y="-1167" type="curve"/>
+      <point x="429" y="-1137" type="line"/>
+      <point x="234" y="-899"/>
+      <point x="136" y="-574"/>
+      <point x="136" y="-241" type="curve" smooth="yes"/>
+      <point x="136" y="92"/>
+      <point x="234" y="417"/>
+      <point x="429" y="657" type="curve"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size3.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size3.glif
new file mode 100644
index 0000000..8ef2734
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size3.glif
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.size3" format="2">
+  <advance width="561"/>
+  <outline>
+    <contour>
+      <point x="475" y="685" type="line"/>
+      <point x="220" y="354"/>
+      <point x="90" y="-96"/>
+      <point x="90" y="-552" type="curve" smooth="yes"/>
+      <point x="90" y="-1008"/>
+      <point x="220" y="-1458"/>
+      <point x="475" y="-1788" type="curve"/>
+      <point x="511" y="-1760" type="line"/>
+      <point x="262" y="-1438"/>
+      <point x="136" y="-1000"/>
+      <point x="136" y="-552" type="curve" smooth="yes"/>
+      <point x="136" y="-104"/>
+      <point x="262" y="334"/>
+      <point x="511" y="657" type="curve"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size4.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size4.glif
new file mode 100644
index 0000000..ff2c36b
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.size4.glif
@@ -0,0 +1,22 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.size4" format="2">
+  <advance width="645"/>
+  <outline>
+    <contour>
+      <point x="559" y="685" type="line"/>
+      <point x="248" y="275"/>
+      <point x="90" y="-282"/>
+      <point x="90" y="-847" type="curve" smooth="yes"/>
+      <point x="90" y="-1412"/>
+      <point x="247" y="-1969"/>
+      <point x="559" y="-2377" type="curve"/>
+      <point x="595" y="-2349" type="line"/>
+      <point x="291" y="-1949"/>
+      <point x="136" y="-1404"/>
+      <point x="136" y="-847" type="curve" smooth="yes"/>
+      <point x="136" y="-290"/>
+      <point x="290" y="255"/>
+      <point x="595" y="657" type="curve"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.top.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.top.glif
new file mode 100644
index 0000000..faed22d
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenleft.top.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenleft.top" format="2">
+  <advance width="645"/>
+  <outline>
+    <component base="parenleft.bot" yScale="-1" yOffset="-496"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.bot.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.bot.glif
new file mode 100644
index 0000000..f142d88
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.bot.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.bot" format="2">
+  <advance width="645"/>
+  <outline>
+    <component base="parenleft.bot" xScale="-1" xOffset="645"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.ext.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.ext.glif
new file mode 100644
index 0000000..dfa357d
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.ext.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.ext" format="2">
+  <advance width="645"/>
+  <outline>
+    <component base="parenleft.ext" xOffset="419"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.glif
new file mode 100644
index 0000000..04ce416
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.glif
@@ -0,0 +1,44 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright" format="2">
+  <advance width="331"/>
+  <unicode hex="0029"/>
+  <outline>
+    <component base="parenleft" xScale="-1" xOffset="331"/>
+  </outline>
+  <lib>
+    <dict>
+      <key>com.nagwa.MATHPlugin.variants</key>
+      <dict>
+        <key>vAssembly</key>
+        <array>
+          <array>
+            <string>parenright.bot</string>
+            <integer>0</integer>
+            <integer>0</integer>
+            <integer>314</integer>
+          </array>
+          <array>
+            <string>parenright.ext</string>
+            <integer>1</integer>
+            <integer>630</integer>
+            <integer>630</integer>
+          </array>
+          <array>
+            <string>parenright.top</string>
+            <integer>0</integer>
+            <integer>314</integer>
+            <integer>0</integer>
+          </array>
+        </array>
+        <key>vVariants</key>
+        <array>
+          <string>parenright</string>
+          <string>parenright.size1</string>
+          <string>parenright.size2</string>
+          <string>parenright.size3</string>
+          <string>parenright.size4</string>
+        </array>
+      </dict>
+    </dict>
+  </lib>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size1.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size1.glif
new file mode 100644
index 0000000..df14053
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size1.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.size1" format="2">
+  <advance width="416"/>
+  <outline>
+    <component base="parenleft.size1" xScale="-1" xOffset="416"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size2.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size2.glif
new file mode 100644
index 0000000..7993e2a
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size2.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.size2" format="2">
+  <advance width="479"/>
+  <outline>
+    <component base="parenleft.size2" xScale="-1" xOffset="479"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size3.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size3.glif
new file mode 100644
index 0000000..6db1980
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size3.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.size3" format="2">
+  <advance width="561"/>
+  <outline>
+    <component base="parenleft.size3" xScale="-1" xOffset="561"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size4.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size4.glif
new file mode 100644
index 0000000..a481dcd
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.size4.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.size4" format="2">
+  <advance width="645"/>
+  <outline>
+    <component base="parenleft.size4" xScale="-1" xOffset="645"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.top.glif b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.top.glif
new file mode 100644
index 0000000..29212ee
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/glyphs/parenright.top.glif
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name="parenright.top" format="2">
+  <advance width="645"/>
+  <outline>
+    <component base="parenleft.top" xScale="-1" xOffset="645"/>
+  </outline>
+</glyph>
diff --git a/tests/data/TestMathFont-Regular.ufo/layercontents.plist b/tests/data/TestMathFont-Regular.ufo/layercontents.plist
new file mode 100644
index 0000000..b9c1a4f
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/layercontents.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <array>
+    <array>
+      <string>public.default</string>
+      <string>glyphs</string>
+    </array>
+  </array>
+</plist>
diff --git a/tests/data/TestMathFont-Regular.ufo/lib.plist b/tests/data/TestMathFont-Regular.ufo/lib.plist
new file mode 100644
index 0000000..74ccf15
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/lib.plist
@@ -0,0 +1,147 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>com.github.googlei18n.ufo2ft.useProductionNames</key>
+    <false/>
+    <key>com.nagwa.MATHPlugin.constants</key>
+    <dict>
+      <key>AccentBaseHeight</key>
+      <integer>449</integer>
+      <key>AxisHeight</key>
+      <integer>250</integer>
+      <key>DelimitedSubFormulaMinHeight</key>
+      <integer>1010</integer>
+      <key>DisplayOperatorMinHeight</key>
+      <integer>2260</integer>
+      <key>FlattenedAccentBaseHeight</key>
+      <integer>656</integer>
+      <key>FractionDenomDisplayStyleGapMin</key>
+      <integer>137</integer>
+      <key>FractionDenominatorDisplayStyleShiftDown</key>
+      <integer>685</integer>
+      <key>FractionDenominatorGapMin</key>
+      <integer>45</integer>
+      <key>FractionDenominatorShiftDown</key>
+      <integer>345</integer>
+      <key>FractionNumDisplayStyleGapMin</key>
+      <integer>137</integer>
+      <key>FractionNumeratorDisplayStyleShiftUp</key>
+      <integer>677</integer>
+      <key>FractionNumeratorGapMin</key>
+      <integer>45</integer>
+      <key>FractionNumeratorShiftUp</key>
+      <integer>394</integer>
+      <key>FractionRuleThickness</key>
+      <integer>45</integer>
+      <key>LowerLimitBaselineDropMin</key>
+      <integer>600</integer>
+      <key>LowerLimitGapMin</key>
+      <integer>166</integer>
+      <key>MinConnectorOverlap</key>
+      <integer>20</integer>
+      <key>OverbarExtraAscender</key>
+      <integer>45</integer>
+      <key>OverbarRuleThickness</key>
+      <integer>45</integer>
+      <key>OverbarVerticalGap</key>
+      <integer>137</integer>
+      <key>RadicalDegreeBottomRaisePercent</key>
+      <integer>60</integer>
+      <key>RadicalDisplayStyleVerticalGap</key>
+      <integer>158</integer>
+      <key>RadicalExtraAscender</key>
+      <integer>70</integer>
+      <key>RadicalKernAfterDegree</key>
+      <integer>-400</integer>
+      <key>RadicalKernBeforeDegree</key>
+      <integer>277</integer>
+      <key>RadicalRuleThickness</key>
+      <integer>45</integer>
+      <key>RadicalVerticalGap</key>
+      <integer>57</integer>
+      <key>ScriptPercentScaleDown</key>
+      <integer>70</integer>
+      <key>ScriptScriptPercentScaleDown</key>
+      <integer>50</integer>
+      <key>SkewedFractionHorizontalGap</key>
+      <integer>400</integer>
+      <key>SkewedFractionVerticalGap</key>
+      <integer>60</integer>
+      <key>SpaceAfterScript</key>
+      <integer>50</integer>
+      <key>StackBottomDisplayStyleShiftDown</key>
+      <integer>685</integer>
+      <key>StackBottomShiftDown</key>
+      <integer>345</integer>
+      <key>StackDisplayStyleGapMin</key>
+      <integer>321</integer>
+      <key>StackGapMin</key>
+      <integer>137</integer>
+      <key>StackTopDisplayStyleShiftUp</key>
+      <integer>677</integer>
+      <key>StackTopShiftUp</key>
+      <integer>444</integer>
+      <key>StretchStackBottomShiftDown</key>
+      <integer>600</integer>
+      <key>StretchStackGapAboveMin</key>
+      <integer>111</integer>
+      <key>StretchStackGapBelowMin</key>
+      <integer>166</integer>
+      <key>StretchStackTopShiftUp</key>
+      <integer>199</integer>
+      <key>SubSuperscriptGapMin</key>
+      <integer>183</integer>
+      <key>SubscriptBaselineDropMin</key>
+      <integer>50</integer>
+      <key>SubscriptShiftDown</key>
+      <integer>149</integer>
+      <key>SubscriptTopMax</key>
+      <integer>359</integer>
+      <key>SuperscriptBaselineDropMax</key>
+      <integer>385</integer>
+      <key>SuperscriptBottomMaxWithSubscript</key>
+      <integer>359</integer>
+      <key>SuperscriptBottomMin</key>
+      <integer>112</integer>
+      <key>SuperscriptShiftUp</key>
+      <integer>362</integer>
+      <key>SuperscriptShiftUpCramped</key>
+      <integer>289</integer>
+      <key>UnderbarExtraDescender</key>
+      <integer>45</integer>
+      <key>UnderbarRuleThickness</key>
+      <integer>45</integer>
+      <key>UnderbarVerticalGap</key>
+      <integer>137</integer>
+      <key>UpperLimitBaselineRiseMin</key>
+      <integer>199</integer>
+      <key>UpperLimitGapMin</key>
+      <integer>111</integer>
+    </dict>
+    <key>com.nagwa.MATHPlugin.extendedShape</key>
+    <array>
+      <string>parenleft</string>
+      <string>parenright</string>
+    </array>
+    <key>public.glyphOrder</key>
+    <array>
+      <string>parenleft</string>
+      <string>parenleft.bot</string>
+      <string>parenleft.ext</string>
+      <string>parenleft.size1</string>
+      <string>parenleft.size2</string>
+      <string>parenleft.size3</string>
+      <string>parenleft.size4</string>
+      <string>parenleft.top</string>
+      <string>parenright</string>
+      <string>parenright.bot</string>
+      <string>parenright.ext</string>
+      <string>parenright.size1</string>
+      <string>parenright.size2</string>
+      <string>parenright.size3</string>
+      <string>parenright.size4</string>
+      <string>parenright.top</string>
+    </array>
+  </dict>
+</plist>
diff --git a/tests/data/TestMathFont-Regular.ufo/metainfo.plist b/tests/data/TestMathFont-Regular.ufo/metainfo.plist
new file mode 100644
index 0000000..7b8b34a
--- /dev/null
+++ b/tests/data/TestMathFont-Regular.ufo/metainfo.plist
@@ -0,0 +1,10 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>creator</key>
+    <string>com.github.fonttools.ufoLib</string>
+    <key>formatVersion</key>
+    <integer>3</integer>
+  </dict>
+</plist>
diff --git a/tests/outlineCompiler_test.py b/tests/outlineCompiler_test.py
index df52f4b..86cc1b2 100644
--- a/tests/outlineCompiler_test.py
+++ b/tests/outlineCompiler_test.py
@@ -18,6 +18,9 @@ from ufo2ft import (
 )
 from ufo2ft.constants import (
     GLYPHS_DONT_USE_PRODUCTION_NAMES,
+    GLYPHS_MATH_CONSTANTS_KEY,
+    GLYPHS_MATH_EXTENDED_SHAPE_KEY,
+    GLYPHS_MATH_VARIANTS_KEY,
     OPENTYPE_POST_UNDERLINE_POSITION_KEY,
     SPARSE_OTF_MASTER_TABLES,
     SPARSE_TTF_MASTER_TABLES,
@@ -1403,6 +1406,28 @@ def test_achVendId_space_padded_if_less_than_4_chars(
     assert font["OS/2"].achVendID == expected
 
 
+def test_MATH_table(FontClass):
+    ufo = FontClass(getpath("TestMathFont-Regular.ufo"))
+    result = compileTTF(ufo)
+    assert "MATH" in result
+
+    math = result["MATH"].table
+
+    for key, value in ufo.lib[GLYPHS_MATH_CONSTANTS_KEY].items():
+        attr = getattr(math.MathConstants, key)
+        if isinstance(attr, int):
+            assert attr == value
+        else:
+            assert attr.Value == value
+
+    extendedShapes = set(ufo.lib[GLYPHS_MATH_EXTENDED_SHAPE_KEY])
+    for glyph in ufo.lib[GLYPHS_MATH_EXTENDED_SHAPE_KEY]:
+        if variants := ufo[glyph].lib.get(GLYPHS_MATH_VARIANTS_KEY):
+            extendedShapes.update(variants.get("vVariants", []))
+
+    assert set(math.MathGlyphInfo.ExtendedShapeCoverage.glyphs) == extendedShapes
+
+
 if __name__ == "__main__":
     import sys