Repository: googlefonts/ufo2ft
Pull request #387 (2020-06-05 18:32:43)

Take .notdef outline from the default master, add notdefOutline param
Fixes https://github.com/googlefonts/ufo2ft/issues/381


Patch:
diff --git a/Lib/ufo2ft/__init__.py b/Lib/ufo2ft/__init__.py
index ec05afc..a317d6b 100644
--- a/Lib/ufo2ft/__init__.py
+++ b/Lib/ufo2ft/__init__.py
@@ -18,7 +18,7 @@ from ufo2ft.featureCompiler import (
 from ufo2ft.outlineCompiler import OutlineOTFCompiler, OutlineTTFCompiler
 from ufo2ft.postProcessor import PostProcessor
 from ufo2ft.constants import SPARSE_TTF_MASTER_TABLES, SPARSE_OTF_MASTER_TABLES
-from ufo2ft.util import getDefaultMasterFont
+from ufo2ft.util import getDefaultMasterFont, _getDefaultNotdefGlyph
 import logging
 
 try:
@@ -54,6 +54,7 @@ def compileOTF(
     debugFeatureFile=None,
     cffVersion=1,
     subroutinizer=None,
+    notdefGlyph=None,
     _tables=None,
 ):
     """Create FontTools CFF font from a UFO.
@@ -128,6 +129,7 @@ def compileOTF(
         ufo,
         glyphSet=glyphSet,
         glyphOrder=glyphOrder,
+        notdefGlyph=notdefGlyph,
         roundTolerance=roundTolerance,
         optimizeCFF=optimizeCFF >= CFFOptimization.SPECIALIZE,
         tables=_tables,
@@ -174,6 +176,7 @@ def compileTTF(
     layerName=None,
     skipExportGlyphs=None,
     debugFeatureFile=None,
+    notdefGlyph=None,
 ):
     """Create FontTools TrueType font from a UFO.
 
@@ -213,7 +216,7 @@ def compileTTF(
 
     logger.info("Building OpenType tables")
     outlineCompiler = outlineCompilerClass(
-        ufo, glyphSet=glyphSet, glyphOrder=glyphOrder
+        ufo, glyphSet=glyphSet, glyphOrder=glyphOrder, notdefGlyph=notdefGlyph
     )
     otf = outlineCompiler.compile()
 
@@ -248,6 +251,7 @@ def compileInterpolatableTTFs(
     layerNames=None,
     skipExportGlyphs=None,
     debugFeatureFile=None,
+    notdefGlyph=None,
 ):
     """Create FontTools TrueType fonts from a list of UFOs with interpolatable
     outlines. Cubic curves are converted compatibly to quadratic curves using
@@ -303,6 +307,7 @@ def compileInterpolatableTTFs(
             ufo,
             glyphSet=glyphSet,
             glyphOrder=glyphOrder,
+            notdefGlyph=notdefGlyph,
             tables=SPARSE_TTF_MASTER_TABLES if layerName else None,
         )
         ttf = outlineCompiler.compile()
@@ -350,6 +355,7 @@ def compileInterpolatableTTFsFromDS(
     reverseDirection=True,
     inplace=False,
     debugFeatureFile=None,
+    notdefGlyph=None,
 ):
     """Create FontTools TrueType fonts from the DesignSpaceDocument UFO sources
     with interpolatable outlines. Cubic curves are converted compatibly to
@@ -388,6 +394,9 @@ def compileInterpolatableTTFsFromDS(
 
     skipExportGlyphs = designSpaceDoc.lib.get("public.skipExportGlyphs", [])
 
+    if notdefGlyph is None:
+        notdefGlyph = _getDefaultNotdefGlyph(designSpaceDoc)
+
     ttfs = compileInterpolatableTTFs(
         ufos,
         preProcessorClass=preProcessorClass,
@@ -402,6 +411,7 @@ def compileInterpolatableTTFsFromDS(
         layerNames=layerNames,
         skipExportGlyphs=skipExportGlyphs,
         debugFeatureFile=debugFeatureFile,
+        notdefGlyph=notdefGlyph,
     )
 
     if inplace:
@@ -425,6 +435,7 @@ def compileInterpolatableOTFsFromDS(
     roundTolerance=None,
     inplace=False,
     debugFeatureFile=None,
+    notdefGlyph=None,
 ):
     """Create FontTools CFF fonts from the DesignSpaceDocument UFO sources
     with interpolatable outlines.
@@ -461,6 +472,9 @@ def compileInterpolatableOTFsFromDS(
 
     skipExportGlyphs = designSpaceDoc.lib.get("public.skipExportGlyphs", [])
 
+    if notdefGlyph is None:
+        notdefGlyph = _getDefaultNotdefGlyph(designSpaceDoc)
+
     otfs = []
     for source in designSpaceDoc.sources:
         otfs.append(
@@ -480,6 +494,7 @@ def compileInterpolatableOTFsFromDS(
                 inplace=inplace,
                 skipExportGlyphs=skipExportGlyphs,
                 debugFeatureFile=debugFeatureFile,
+                notdefGlyph=notdefGlyph,
                 _tables=SPARSE_OTF_MASTER_TABLES if source.layerName else None,
             )
         )
@@ -555,6 +570,7 @@ def compileVariableTTF(
     optimizeGvar=True,
     inplace=False,
     debugFeatureFile=None,
+    notdefGlyph=None,
 ):
     """Create FontTools TrueType variable font from the DesignSpaceDocument UFO sources
     with interpolatable outlines, using fontTools.varLib.build.
@@ -583,6 +599,7 @@ def compileVariableTTF(
         reverseDirection=reverseDirection,
         inplace=inplace,
         debugFeatureFile=debugFeatureFile,
+        notdefGlyph=notdefGlyph,
     )
 
     logger.info("Building variable TTF font")
@@ -610,6 +627,7 @@ def compileVariableCFF2(
     inplace=False,
     debugFeatureFile=None,
     optimizeCFF=CFFOptimization.SPECIALIZE,
+    notdefGlyph=None,
 ):
     """Create FontTools CFF2 variable font from the DesignSpaceDocument UFO sources
     with interpolatable outlines, using fontTools.varLib.build.
@@ -642,6 +660,7 @@ def compileVariableCFF2(
         roundTolerance=roundTolerance,
         inplace=inplace,
         debugFeatureFile=debugFeatureFile,
+        notdefGlyph=notdefGlyph,
     )
 
     logger.info("Building variable CFF2 font")
diff --git a/Lib/ufo2ft/outlineCompiler.py b/Lib/ufo2ft/outlineCompiler.py
index 56a759f..c748c36 100644
--- a/Lib/ufo2ft/outlineCompiler.py
+++ b/Lib/ufo2ft/outlineCompiler.py
@@ -39,6 +39,7 @@ from ufo2ft.util import (
     makeOfficialGlyphOrder,
     makeUnicodeToGlyphNameMapping,
     calcCodePageRanges,
+    _copyGlyph,
 )
 from ufo2ft.constants import COLOR_LAYERS_KEY, COLOR_PALETTES_KEY
 
@@ -76,12 +77,19 @@ class BaseOutlineCompiler(object):
          "COLR", "CPAL"]
     )
 
-    def __init__(self, font, glyphSet=None, glyphOrder=None, tables=None):
+    def __init__(
+        self,
+        font,
+        glyphSet=None,
+        glyphOrder=None,
+        tables=None,
+        notdefGlyph=None,
+    ):
         self.ufo = font
         # use the previously filtered glyphSet, if any
         if glyphSet is None:
             glyphSet = {g.name: g for g in font}
-        self.makeMissingRequiredGlyphs(font, glyphSet, self.sfntVersion)
+        self.makeMissingRequiredGlyphs(font, glyphSet, self.sfntVersion, notdefGlyph)
         self.allGlyphs = glyphSet
         # store the glyph order
         if glyphOrder is None:
@@ -208,7 +216,7 @@ class BaseOutlineCompiler(object):
         return makeUnicodeToGlyphNameMapping(self.allGlyphs, self.glyphOrder)
 
     @staticmethod
-    def makeMissingRequiredGlyphs(font, glyphSet, sfntVersion):
+    def makeMissingRequiredGlyphs(font, glyphSet, sfntVersion, notdefGlyph=None):
         """
         Add .notdef to the glyph set if it is not present.
 
@@ -219,18 +227,24 @@ class BaseOutlineCompiler(object):
         if ".notdef" in glyphSet:
             return
 
-        unitsPerEm = otRound(getAttrWithFallback(font.info, "unitsPerEm"))
-        ascender = otRound(getAttrWithFallback(font.info, "ascender"))
-        descender = otRound(getAttrWithFallback(font.info, "descender"))
-        defaultWidth = otRound(unitsPerEm * 0.5)
-        glyphSet[".notdef"] = StubGlyph(
-            name=".notdef",
-            width=defaultWidth,
-            unitsPerEm=unitsPerEm,
-            ascender=ascender,
-            descender=descender,
-            reverseContour=(sfntVersion == "\000\001\000\000")
-        )
+        reverseContour = sfntVersion == "\000\001\000\000"
+        if notdefGlyph:
+            notdefGlyph = _copyGlyph(notdefGlyph, reverseContour=reverseContour)
+        else:
+            unitsPerEm = otRound(getAttrWithFallback(font.info, "unitsPerEm"))
+            ascender = otRound(getAttrWithFallback(font.info, "ascender"))
+            descender = otRound(getAttrWithFallback(font.info, "descender"))
+            defaultWidth = otRound(unitsPerEm * 0.5)
+            notdefGlyph = StubGlyph(
+                name=".notdef",
+                width=defaultWidth,
+                unitsPerEm=unitsPerEm,
+                ascender=ascender,
+                descender=descender,
+                reverseContour=reverseContour,
+            )
+
+        glyphSet[".notdef"] = notdefGlyph
 
     def makeOfficialGlyphOrder(self, glyphOrder):
         """
@@ -976,6 +990,7 @@ class OutlineOTFCompiler(BaseOutlineCompiler):
         glyphSet=None,
         glyphOrder=None,
         tables=None,
+        notdefGlyph=None,
         roundTolerance=None,
         optimizeCFF=True,
     ):
@@ -985,7 +1000,11 @@ class OutlineOTFCompiler(BaseOutlineCompiler):
             # round all coordinates to integers by default
             self.roundTolerance = 0.5
         super(OutlineOTFCompiler, self).__init__(
-            font, glyphSet=glyphSet, glyphOrder=glyphOrder, tables=tables
+            font,
+            glyphSet=glyphSet,
+            glyphOrder=glyphOrder,
+            tables=tables,
+            notdefGlyph=notdefGlyph,
         )
         self.optimizeCFF = optimizeCFF
         self._defaultAndNominalWidths = None
diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py
index d6e4122..31e0681 100644
--- a/Lib/ufo2ft/util.py
+++ b/Lib/ufo2ft/util.py
@@ -90,13 +90,21 @@ class _GlyphSet(dict):
 
 
 def _copyLayer(layer, obj_type=dict):
-    # defcon.Glyph doesn't take a name argument, ufoLib2 requires one...
     try:
         g = next(iter(layer))
     except StopIteration:  # layer is empty
         return obj_type()
 
-    cls = g.__class__
+    newGlyph = _getNewGlyphFactory(g)
+    glyphSet = obj_type()
+    for glyph in layer:
+        glyphSet[glyph.name] = _copyGlyph(glyph, glyphFactory=newGlyph)
+    return glyphSet
+
+
+def _getNewGlyphFactory(glyph):
+    # defcon.Glyph doesn't take a name argument, ufoLib2 requires one...
+    cls = glyph.__class__
     if "name" in getargspec(cls.__init__).args:
 
         def newGlyph(name):
@@ -107,23 +115,34 @@ def _copyLayer(layer, obj_type=dict):
         def newGlyph(name):
             # use instantiateGlyphObject() to keep any custom sub-element classes
             # https://github.com/googlefonts/ufo2ft/issues/363
-            g2 = g.layer.instantiateGlyphObject()
+            g2 = glyph.layer.instantiateGlyphObject()
             g2.name = name
             return g2
 
+    return newGlyph
+
+
+def _copyGlyph(glyph, glyphFactory=None, reverseContour=False):
     # copy everything except unused attributes: 'guidelines', 'note', 'image'
-    glyphSet = obj_type()
-    for glyph in layer:
-        copy = newGlyph(glyph.name)
-        copy.width = glyph.width
-        copy.height = glyph.height
-        copy.unicodes = list(glyph.unicodes)
-        copy.anchors = [dict(a) for a in glyph.anchors]
-        copy.lib = deepcopy(glyph.lib)
-        pointPen = copy.getPointPen()
-        glyph.drawPoints(pointPen)
-        glyphSet[glyph.name] = copy
-    return glyphSet
+    if glyphFactory is None:
+        glyphFactory = _getNewGlyphFactory(glyph)
+
+    copy = glyphFactory(glyph.name)
+    copy.width = glyph.width
+    copy.height = glyph.height
+    copy.unicodes = list(glyph.unicodes)
+    copy.anchors = [dict(a) for a in glyph.anchors]
+    copy.lib = deepcopy(glyph.lib)
+
+    pointPen = copy.getPointPen()
+    if reverseContour:
+        from fontTools.pens.pointPen import ReverseContourPointPen
+
+        pointPen = ReverseContourPointPen(pointPen)
+
+    glyph.drawPoints(pointPen)
+
+    return copy
 
 
 def deepCopyContours(
@@ -393,3 +412,19 @@ def getDefaultMasterFont(designSpaceDoc):
             % getattr(defaultSource, "name", "<Unknown>")
         )
     return defaultSource.font
+
+
+def _getDefaultNotdefGlyph(designSpaceDoc):
+    from ufo2ft.errors import InvalidDesignSpaceData
+
+    try:
+        baseUfo = getDefaultMasterFont(designSpaceDoc)
+    except InvalidDesignSpaceData:
+        notdefGlyph = None
+    else:
+        # unlike ufoLib2, defcon has no Font.get() method
+        try:
+            notdefGlyph = baseUfo[".notdef"]
+        except KeyError:
+            notdefGlyph = None
+    return notdefGlyph


Test patch:
diff --git a/tests/data/LayerFont-Bold.ufo/glyphs/_notdef.glif b/tests/data/LayerFont-Bold.ufo/glyphs/_notdef.glif
new file mode 100644
index 0000000..c97538b
--- /dev/null
+++ b/tests/data/LayerFont-Bold.ufo/glyphs/_notdef.glif
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name=".notdef" format="2">
+  <advance height="1000" width="500"/>
+  <outline>
+    <contour>
+      <point x="50" y="-250" type="line"/>
+      <point x="450" y="-250" type="line"/>
+      <point x="450" y="750" type="line"/>
+      <point x="50" y="750" type="line"/>
+    </contour>
+    <contour>
+      <point x="100" y="-200" type="line"/>
+      <point x="100" y="700" type="line"/>
+      <point x="400" y="700" type="line"/>
+      <point x="400" y="-200" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/LayerFont-Bold.ufo/glyphs/contents.plist b/tests/data/LayerFont-Bold.ufo/glyphs/contents.plist
index 326f0a0..66127d6 100644
--- a/tests/data/LayerFont-Bold.ufo/glyphs/contents.plist
+++ b/tests/data/LayerFont-Bold.ufo/glyphs/contents.plist
@@ -2,6 +2,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>.notdef</key>
+    <string>_notdef.glif</string>
     <key>a</key>
     <string>a.glif</string>
     <key>dotabovecomb</key>
diff --git a/tests/data/LayerFont-Regular.ufo/glyphs/_notdef.glif b/tests/data/LayerFont-Regular.ufo/glyphs/_notdef.glif
new file mode 100644
index 0000000..c97538b
--- /dev/null
+++ b/tests/data/LayerFont-Regular.ufo/glyphs/_notdef.glif
@@ -0,0 +1,18 @@
+<?xml version='1.0' encoding='UTF-8'?>
+<glyph name=".notdef" format="2">
+  <advance height="1000" width="500"/>
+  <outline>
+    <contour>
+      <point x="50" y="-250" type="line"/>
+      <point x="450" y="-250" type="line"/>
+      <point x="450" y="750" type="line"/>
+      <point x="50" y="750" type="line"/>
+    </contour>
+    <contour>
+      <point x="100" y="-200" type="line"/>
+      <point x="100" y="700" type="line"/>
+      <point x="400" y="700" type="line"/>
+      <point x="400" y="-200" type="line"/>
+    </contour>
+  </outline>
+</glyph>
diff --git a/tests/data/LayerFont-Regular.ufo/glyphs/contents.plist b/tests/data/LayerFont-Regular.ufo/glyphs/contents.plist
index 326f0a0..66127d6 100644
--- a/tests/data/LayerFont-Regular.ufo/glyphs/contents.plist
+++ b/tests/data/LayerFont-Regular.ufo/glyphs/contents.plist
@@ -2,6 +2,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>.notdef</key>
+    <string>_notdef.glif</string>
     <key>a</key>
     <string>a.glif</string>
     <key>dotabovecomb</key>
diff --git a/tests/data/TestVariableFont-CFF2-cffsubr.ttx b/tests/data/TestVariableFont-CFF2-cffsubr.ttx
index b84ce0b..d5ec559 100644
--- a/tests/data/TestVariableFont-CFF2-cffsubr.ttx
+++ b/tests/data/TestVariableFont-CFF2-cffsubr.ttx
@@ -222,9 +222,9 @@
       <CharStrings>
         <CharString name=".notdef">
           50 -250 rmoveto
-          400 1000 -400 -1000 hlineto
-          50 50 rmoveto
-          900 300 -900 -300 vlineto
+          400 1000 -400 hlineto
+          50 -950 rmoveto
+          900 300 -900 vlineto
         </CharString>
         <CharString name="a">
           468 -1 rmoveto
diff --git a/tests/data/TestVariableFont-CFF2-useProductionNames.ttx b/tests/data/TestVariableFont-CFF2-useProductionNames.ttx
index 639c5c3..3015fd0 100644
--- a/tests/data/TestVariableFont-CFF2-useProductionNames.ttx
+++ b/tests/data/TestVariableFont-CFF2-useProductionNames.ttx
@@ -210,9 +210,9 @@
       <CharStrings>
         <CharString name=".notdef">
           50 -250 rmoveto
-          400 1000 -400 -1000 hlineto
-          50 50 rmoveto
-          900 300 -900 -300 vlineto
+          400 1000 -400 hlineto
+          50 -950 rmoveto
+          900 300 -900 vlineto
         </CharString>
         <CharString name="uni0061">
           468 -1 rmoveto
diff --git a/tests/data/TestVariableFont-CFF2.ttx b/tests/data/TestVariableFont-CFF2.ttx
index ddb4feb..310c85a 100644
--- a/tests/data/TestVariableFont-CFF2.ttx
+++ b/tests/data/TestVariableFont-CFF2.ttx
@@ -207,9 +207,9 @@
       <CharStrings>
         <CharString name=".notdef">
           50 -250 rmoveto
-          400 1000 -400 -1000 hlineto
-          50 50 rmoveto
-          900 300 -900 -300 vlineto
+          400 1000 -400 hlineto
+          50 -950 rmoveto
+          900 300 -900 vlineto
         </CharString>
         <CharString name="a">
           468 -1 rmoveto