Repository: fonttools/fonttools

Issue:
[4.39+] Composite TrueType instructions are discarded when dumping to XML
I found a serious bug that apparently was introduced in release 4.39.0.

In the attached demo font, the B glyph is a composite which has TrueType instructions. When dumped with ttx in FontTools 4.39.0+, the instructions of any composites are missing from the ttx file.

[2023-composite-hints.zip](https://github.com/fonttools/fonttools/files/11322045/2023-composite-hints.zip)



Discussion:

Resolving patch:
diff --git a/Lib/fontTools/ttLib/tables/_g_l_y_f.py b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
--- a/Lib/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/Lib/fontTools/ttLib/tables/_g_l_y_f.py
@@ -746,7 +746,7 @@ def toXML(self, writer, ttFont):
             for compo in self.components:
                 compo.toXML(writer, ttFont)
             haveInstructions = hasattr(self, "program")
-        if self.isVarComposite():
+        elif self.isVarComposite():
             for compo in self.components:
                 compo.toXML(writer, ttFont)
             haveInstructions = False


Test patch:
diff --git a/Tests/ttLib/tables/_g_l_y_f_test.py b/Tests/ttLib/tables/_g_l_y_f_test.py
--- a/Tests/ttLib/tables/_g_l_y_f_test.py
+++ b/Tests/ttLib/tables/_g_l_y_f_test.py
@@ -186,6 +186,7 @@ def test__checkFloat_overflow(self):
 HEAD_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.head.bin")
 LOCA_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.loca.bin")
 MAXP_BIN = os.path.join(DATA_DIR, "_g_l_y_f_outline_flag_bit6.maxp.bin")
+INST_TTX = os.path.join(DATA_DIR, "_g_l_y_f_instructions.ttx")
 
 
 def strip_ttLibVersion(string):
@@ -235,6 +236,18 @@ def test_fromXML(self):
         glyfData = glyfTable.compile(font)
         self.assertEqual(glyfData, self.glyfData)
 
+    def test_instructions_roundtrip(self):
+        font = TTFont(sfntVersion="\x00\x01\x00\x00")
+        font.importXML(INST_TTX)
+        glyfTable = font["glyf"]
+        self.glyfData = glyfTable.compile(font)
+        out = StringIO()
+        font.saveXML(out)
+        glyfXML = strip_ttLibVersion(out.getvalue()).splitlines()
+        with open(INST_TTX, "r") as f:
+            origXML = strip_ttLibVersion(f.read()).splitlines()
+        self.assertEqual(glyfXML, origXML)
+
     def test_recursiveComponent(self):
         glyphSet = {}
         pen_dummy = TTGlyphPen(glyphSet)
diff --git a/Tests/ttLib/tables/data/_g_l_y_f_instructions.ttx b/Tests/ttLib/tables/data/_g_l_y_f_instructions.ttx
new file mode 100644
--- /dev/null
+++ b/Tests/ttLib/tables/data/_g_l_y_f_instructions.ttx
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.39">
+
+  <GlyphOrder>
+    <!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
+    <GlyphID id="0" name=".notdef"/>
+    <GlyphID id="1" name="NULL"/>
+    <GlyphID id="2" name="nonmarkingreturn"/>
+    <GlyphID id="3" name="A"/>
+    <GlyphID id="4" name="B"/>
+  </GlyphOrder>
+
+  <maxp>
+    <!-- Most of this table will be recalculated by the compiler -->
+    <tableVersion value="0x10000"/>
+    <numGlyphs value="5"/>
+    <maxPoints value="4"/>
+    <maxContours value="1"/>
+    <maxCompositePoints value="4"/>
+    <maxCompositeContours value="1"/>
+    <maxZones value="1"/>
+    <maxTwilightPoints value="0"/>
+    <maxStorage value="0"/>
+    <maxFunctionDefs value="10"/>
+    <maxInstructionDefs value="0"/>
+    <maxStackElements value="512"/>
+    <maxSizeOfInstructions value="371"/>
+    <maxComponentElements value="1"/>
+    <maxComponentDepth value="1"/>
+  </maxp>
+
+  <loca>
+    <!-- The 'loca' table will be calculated by the compiler -->
+  </loca>
+
+  <glyf>
+
+    <!-- The xMin, yMin, xMax and yMax values
+         will be recalculated by the compiler. -->
+
+    <TTGlyph name=".notdef"/><!-- contains no outline data -->
+
+    <TTGlyph name="A" xMin="100" yMin="0" xMax="477" yMax="700">
+      <contour>
+        <pt x="100" y="700" on="1"/>
+        <pt x="477" y="700" on="1"/>
+        <pt x="477" y="0" on="1"/>
+        <pt x="100" y="0" on="1"/>
+      </contour>
+      <instructions>
+        <assembly>
+          SVTCA[0]	/* SetFPVectorToAxis */
+          PUSHW[ ]	/* 1 value pushed */
+          3
+          MDAP[1]	/* MoveDirectAbsPt */
+          IUP[0]	/* InterpolateUntPts */
+          IUP[1]	/* InterpolateUntPts */
+        </assembly>
+      </instructions>
+    </TTGlyph>
+
+    <TTGlyph name="B" xMin="100" yMin="0" xMax="477" yMax="700">
+      <component glyphName="A" x="0" y="0" flags="0x204"/>
+      <instructions>
+        <assembly>
+          SVTCA[0]	/* SetFPVectorToAxis */
+          PUSHW[ ]	/* 1 value pushed */
+          1
+          MDAP[1]	/* MoveDirectAbsPt */
+          IUP[0]	/* InterpolateUntPts */
+          IUP[1]	/* InterpolateUntPts */
+        </assembly>
+      </instructions>
+    </TTGlyph>
+
+    <TTGlyph name="NULL"/><!-- contains no outline data -->
+
+    <TTGlyph name="nonmarkingreturn"/><!-- contains no outline data -->
+
+  </glyf>
+
+</ttFont>