Repository: fonttools/fonttools

Issue:
ReemKufiInk crashes pyftsubset
```
pyftsubset --text=duck --output-file=/tmp/reem.otf ReemKufiInk-Bold.otf

Traceback (most recent call last):
  File "/tmp/venv/bin/pyftsubset", line 8, in <module>
    sys.exit(main())
  File "/tmp/venv/lib/python3.9/site-packages/fontTools/misc/loggingTools.py", line 372, in wrapper
    return func(*args, **kwds)
  File "/tmp/venv/lib/python3.9/site-packages/fontTools/subset/__init__.py", line 3104, in main
    subsetter.subset(font)
  File "/tmp/venv/lib/python3.9/site-packages/fontTools/subset/__init__.py", line 2908, in subset
    self._subset_glyphs(font)
  File "/tmp/venv/lib/python3.9/site-packages/fontTools/subset/__init__.py", line 2846, in _subset_glyphs
    retain = table.subset_glyphs(self)
  File "/tmp/venv/lib/python3.9/site-packages/fontTools/subset/__init__.py", line 2086, in subset_glyphs
    colorGlyphsV1 = unbuildColrV1(self.table.LayerList, self.table.BaseGlyphList)
  File "/tmp/venv/lib/python3.9/site-packages/fontTools/colorLib/unbuilder.py", line 6, in unbuildColrV1
    unbuilder = LayerListUnbuilder(layerV1List.Paint)
AttributeError: 'NoneType' object has no attribute 'Paint'
```

TTX handles the font just fine. File from https://github.com/aliftype/reem-kufi/commits/colr-v1 at 93d6dcd693ae42bb4295701e88a07cc4d04db73c


Discussion:
Summon @anthrotype 
Oh.. the LayerList is optional so we should check is not None. Strange that the font makes no use of PaintColrLayers, but still valid and subsetter should be fixed
> Strange that the font makes no use of PaintColrLayers

Yes, it is a placeholder font (all COLRv1 layers are single layers with solid colors)
I love that summoning Cosimo actually worked. And we found a bug, win/win!
Original pyftsubset command does now pass but other use still crashes; reopening.

```
pyftsubset --output-file=/tmp/reem.ttf --gids=0,128,210,211,218,225,227,232,234,235,237,238,241,244,246,250,251,254,261,264,270,271,277,280,282,284,286,289,295,297,299,307,308,316,317,318,320,327,331,343,345,347,348,349,350,351,357,365,370,371,374,376,378,392,393,398,399 ReemKufiInk-Regular.otf

Traceback (most recent call last):
  File "venv/bin/pyftsubset", line 11, in <module>
    load_entry_point('fonttools', 'console_scripts', 'pyftsubset')()
  File "Lib/fontTools/misc/loggingTools.py", line 372, in wrapper
    return func(*args, **kwds)
  File "Lib/fontTools/subset/__init__.py", line 3106, in main
    subsetter.subset(font)
  File "Lib/fontTools/subset/__init__.py", line 2911, in subset
    self._prune_post_subset(font)
  File "Lib/fontTools/subset/__init__.py", line 2894, in _prune_post_subset
    retain = table.prune_post_subset(font, self.options)
  File "Lib/fontTools/subset/__init__.py", line 2140, in prune_post_subset
    record.Paint.traverse(colr.table, collect_colors_by_index)
  File "Lib/fontTools/ttLib/tables/otTables.py", line 1550, in traverse
    stack.extend(reversed(current.getChildren(colr)))
  File "Lib/fontTools/ttLib/tables/otTables.py", line 1520, in getChildren
    return colr.LayerList.Paint[
AttributeError: 'NoneType' object has no attribute 'Paint'
```

Resolving patch:
diff --git a/Lib/fontTools/misc/testTools.py b/Lib/fontTools/misc/testTools.py
--- a/Lib/fontTools/misc/testTools.py
+++ b/Lib/fontTools/misc/testTools.py
@@ -38,6 +38,14 @@ def parseXML(xmlSnippet):
     return reader.root[2]
 
 
+def parseXmlInto(font, parseInto, xmlSnippet):
+    parsed_xml = [e for e in parseXML(xmlSnippet.strip()) if not isinstance(e, str)]
+    for name, attrs, content in parsed_xml:
+      parseInto.fromXML(name, attrs, content, font)
+    parseInto.populateDefaults()
+    return parseInto
+
+
 class FakeFont:
     def __init__(self, glyphs):
         self.glyphOrder_ = glyphs
diff --git a/Lib/fontTools/ttLib/tables/otTables.py b/Lib/fontTools/ttLib/tables/otTables.py
--- a/Lib/fontTools/ttLib/tables/otTables.py
+++ b/Lib/fontTools/ttLib/tables/otTables.py
@@ -1516,7 +1516,11 @@ def toXML(self, xmlWriter, font, attrs=None, name=None):
 
 	def getChildren(self, colr):
 		if self.Format == PaintFormat.PaintColrLayers:
-			return colr.LayerList.Paint[
+			# https://github.com/fonttools/fonttools/issues/2438: don't die when no LayerList exists
+			layers = []
+			if colr.LayerList is not None:
+				layers = colr.LayerList.Paint
+			return layers[
 				self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers
 			]
 


Test patch:
diff --git a/Tests/ttLib/tables/otTables_test.py b/Tests/ttLib/tables/otTables_test.py
--- a/Tests/ttLib/tables/otTables_test.py
+++ b/Tests/ttLib/tables/otTables_test.py
@@ -1,4 +1,4 @@
-from fontTools.misc.testTools import getXML, parseXML, FakeFont
+from fontTools.misc.testTools import getXML, parseXML, parseXmlInto, FakeFont
 from fontTools.misc.textTools import deHexStr, hexStr
 from fontTools.misc.xmlWriter import XMLWriter
 from fontTools.ttLib.tables.otBase import OTTableReader, OTTableWriter
@@ -687,6 +687,33 @@ def test_splitMarkBasePos():
     ]
 
 
+class ColrV1Test(unittest.TestCase):
+  def setUp(self):
+    self.font = FakeFont(['.notdef', 'meh'])
+
+  def test_traverseEmptyPaintColrLayersNeedsNoLayerList(self):
+    colr = parseXmlInto(self.font, otTables.COLR(),
+      '''
+      <Version value="1"/>
+      <BaseGlyphList>
+        <BaseGlyphPaintRecord index="0">
+          <BaseGlyph value="meh"/>
+          <Paint Format="1"><!-- PaintColrLayers -->
+            <NumLayers value="0"/>
+            <FirstLayerIndex value="42"/>
+          </Paint>
+        </BaseGlyphPaintRecord>
+      </BaseGlyphList>
+      ''')
+    paint = colr.BaseGlyphList.BaseGlyphPaintRecord[0].Paint
+
+    # Just want to confirm we don't crash
+    visited = []
+    paint.traverse(colr, lambda p: visited.append(p))
+    assert len(visited) == 1
+
+
+
 if __name__ == "__main__":
     import sys
     sys.exit(unittest.main())