Repository: googlefonts/ufo2ft
Pull request #364 (2020-02-20 12:56:01)

Provide fallbacks for info.(unitsPerEm|(style|family)Name|ascender|capHeight|xHeight|descender)
Finally, you too can compile an empty UFO. Fallback values for heights are taken from Glyphs.

I wonder if I now have to change _all_ access to any these new fontinfo attrs to fallback methods.


Patch:
diff --git a/Lib/ufo2ft/featureWriters/markFeatureWriter.py b/Lib/ufo2ft/featureWriters/markFeatureWriter.py
index 3d12b46..125d685 100644
--- a/Lib/ufo2ft/featureWriters/markFeatureWriter.py
+++ b/Lib/ufo2ft/featureWriters/markFeatureWriter.py
@@ -7,6 +7,7 @@ from fontTools.misc.py23 import tostr, tounicode
 from fontTools.misc.fixedTools import otRound
 from ufo2ft.featureWriters import BaseFeatureWriter, ast
 from ufo2ft.util import unicodeInScripts, classifyGlyphs
+from ufo2ft.fontInfoData import getAttrWithFallback
 
 
 class AbstractMarkPos(object):
@@ -570,7 +571,7 @@ class MarkFeatureWriter(BaseFeatureWriter):
         # anchors with unknown names whose Y coordinate is greater or equal to
         # the line that cuts the UPEM square in half will be treated as "above
         # base" marks, those that fall below the threshold as "below base".
-        return self.context.font.info.unitsPerEm // 2
+        return getAttrWithFallback(self.context.font.info, "unitsPerEm") // 2
 
     def _isAboveMark(self, anchor):
         if anchor.name in self.abvmAnchorNames:
diff --git a/Lib/ufo2ft/filters/cubicToQuadratic.py b/Lib/ufo2ft/filters/cubicToQuadratic.py
index 733757e..6347f4d 100644
--- a/Lib/ufo2ft/filters/cubicToQuadratic.py
+++ b/Lib/ufo2ft/filters/cubicToQuadratic.py
@@ -1,5 +1,6 @@
 from __future__ import print_function, division, absolute_import, unicode_literals
 
+from ufo2ft.fontInfoData import getAttrWithFallback
 from ufo2ft.filters import BaseFilter
 from cu2qu.ufo import DEFAULT_MAX_ERR, CURVE_TYPE_LIB_KEY
 from cu2qu.pens import Cu2QuPointPen
@@ -22,7 +23,7 @@ class CubicToQuadraticFilter(BaseFilter):
         ctx = super(CubicToQuadraticFilter, self).set_context(font, glyphSet)
 
         relativeError = self.options.conversionError or DEFAULT_MAX_ERR
-        ctx.absoluteError = relativeError * font.info.unitsPerEm
+        ctx.absoluteError = relativeError * getAttrWithFallback(font.info, "unitsPerEm")
 
         ctx.stats = {}
 
diff --git a/Lib/ufo2ft/filters/transformations.py b/Lib/ufo2ft/filters/transformations.py
index 55d4fea..a7fe88e 100644
--- a/Lib/ufo2ft/filters/transformations.py
+++ b/Lib/ufo2ft/filters/transformations.py
@@ -4,6 +4,7 @@ import math
 from collections import namedtuple
 import logging
 
+from ufo2ft.fontInfoData import getAttrWithFallback
 from ufo2ft.filters import BaseFilter
 
 from fontTools.misc.py23 import round
@@ -64,13 +65,13 @@ class TransformationsFilter(BaseFilter):
         if origin is self.Origin.BASELINE:
             return 0
         elif origin is self.Origin.CAP_HEIGHT:
-            return font.info.capHeight
+            return getAttrWithFallback(font.info, "capHeight")
         elif origin is self.Origin.HALF_CAP_HEIGHT:
-            return otRound(font.info.capHeight / 2)
+            return otRound(getAttrWithFallback(font.info, "capHeight") / 2)
         elif origin is self.Origin.X_HEIGHT:
-            return font.info.xHeight
+            return getAttrWithFallback(font.info, "xHeight")
         elif origin is self.Origin.HALF_X_HEIGHT:
-            return otRound(font.info.xHeight / 2)
+            return otRound(getAttrWithFallback(font.info, "xHeight") / 2)
         else:
             raise AssertionError(origin)
 
diff --git a/Lib/ufo2ft/fontInfoData.py b/Lib/ufo2ft/fontInfoData.py
index 96934df..d8649bb 100644
--- a/Lib/ufo2ft/fontInfoData.py
+++ b/Lib/ufo2ft/fontInfoData.py
@@ -37,6 +37,26 @@ logger = logging.getLogger(__name__)
 _styleMapStyleNames = ["regular", "bold", "italic", "bold italic"]
 
 
+def ascenderFallback(info):
+    upm = getAttrWithFallback(info, "unitsPerEm")
+    return otRound(upm * 0.8)
+
+
+def descenderFallback(info):
+    upm = getAttrWithFallback(info, "unitsPerEm")
+    return -otRound(upm * 0.2)
+
+
+def capHeightFallback(info):
+    upm = getAttrWithFallback(info, "unitsPerEm")
+    return otRound(upm * 0.7)
+
+
+def xHeightFallback(info):
+    upm = getAttrWithFallback(info, "unitsPerEm")
+    return otRound(upm * 0.5)
+
+
 def styleMapFamilyNameFallback(info):
     """
     Fallback to *openTypeNamePreferredFamilyName* if
@@ -100,14 +120,16 @@ def openTypeHheaAscenderFallback(info):
     """
     Fallback to *ascender + typoLineGap*.
     """
-    return info.ascender + getAttrWithFallback(info, "openTypeOS2TypoLineGap")
+    return getAttrWithFallback(info, "ascender") + getAttrWithFallback(
+        info, "openTypeOS2TypoLineGap"
+    )
 
 
 def openTypeHheaDescenderFallback(info):
     """
     Fallback to *descender*.
     """
-    return info.descender
+    return getAttrWithFallback(info, "descender")
 
 
 def openTypeHheaCaretSlopeRiseFallback(info):
@@ -169,14 +191,14 @@ def openTypeNamePreferredFamilyNameFallback(info):
     """
     Fallback to *familyName*.
     """
-    return info.familyName
+    return getAttrWithFallback(info, "familyName")
 
 
 def openTypeNamePreferredSubfamilyNameFallback(info):
     """
     Fallback to *styleName*.
     """
-    return info.styleName
+    return getAttrWithFallback(info, "styleName")
 
 
 def openTypeNameCompatibleFullNameFallback(info):
@@ -209,35 +231,42 @@ def openTypeOS2TypoAscenderFallback(info):
     """
     Fallback to *ascender*.
     """
-    return info.ascender
+    return getAttrWithFallback(info, "ascender")
 
 
 def openTypeOS2TypoDescenderFallback(info):
     """
     Fallback to *descender*.
     """
-    return info.descender
+    return getAttrWithFallback(info, "descender")
 
 
 def openTypeOS2TypoLineGapFallback(info):
     """
     Fallback to *UPM * 1.2 - ascender + descender*, or zero if that's negative.
     """
-    return max(int(info.unitsPerEm * 1.2) - info.ascender + info.descender, 0)
+    return max(
+        int(getAttrWithFallback(info, "unitsPerEm") * 1.2)
+        - getAttrWithFallback(info, "ascender")
+        + getAttrWithFallback(info, "descender"),
+        0,
+    )
 
 
 def openTypeOS2WinAscentFallback(info):
     """
     Fallback to *ascender + typoLineGap*.
     """
-    return info.ascender + getAttrWithFallback(info, "openTypeOS2TypoLineGap")
+    return getAttrWithFallback(info, "ascender") + getAttrWithFallback(
+        info, "openTypeOS2TypoLineGap"
+    )
 
 
 def openTypeOS2WinDescentFallback(info):
     """
     Fallback to *descender*.
     """
-    return abs(info.descender)
+    return abs(getAttrWithFallback(info, "descender"))
 
 
 # postscript
@@ -300,13 +329,13 @@ def postscriptSlantAngleFallback(info):
 def postscriptUnderlineThicknessFallback(info):
     """Return UPM * 0.05 (50 for 1000 UPM) and warn."""
     logger.warning("Underline thickness not set in UFO, defaulting to UPM * 0.05")
-    return info.unitsPerEm * 0.05
+    return getAttrWithFallback(info, "unitsPerEm") * 0.05
 
 
 def postscriptUnderlinePositionFallback(info):
     """Return UPM * -0.075 (-75 for 1000 UPM) and warn."""
     logger.warning("Underline position not set in UFO, defaulting to UPM * -0.075")
-    return info.unitsPerEm * -0.075
+    return getAttrWithFallback(info, "unitsPerEm") * -0.075
 
 
 def postscriptBlueScaleFallback(info):
@@ -341,6 +370,9 @@ staticFallbackData = dict(
     versionMinor=0,
     copyright=None,
     trademark=None,
+    familyName="New Font",
+    styleName="Regular",
+    unitsPerEm=1000,
     italicAngle=0,
     # not needed
     year=None,
@@ -412,6 +444,10 @@ staticFallbackData = dict(
 )
 
 specialFallbacks = dict(
+    ascender=ascenderFallback,
+    descender=descenderFallback,
+    capHeight=capHeightFallback,
+    xHeight=xHeightFallback,
     styleMapFamilyName=styleMapFamilyNameFallback,
     styleMapStyleName=styleMapStyleNameFallback,
     openTypeHeadCreated=openTypeHeadCreatedFallback,
diff --git a/Lib/ufo2ft/preProcessor.py b/Lib/ufo2ft/preProcessor.py
index bf41967..554b924 100644
--- a/Lib/ufo2ft/preProcessor.py
+++ b/Lib/ufo2ft/preProcessor.py
@@ -1,6 +1,7 @@
 from __future__ import print_function, division, absolute_import, unicode_literals
 
 from fontTools.misc.py23 import basestring
+from ufo2ft.fontInfoData import getAttrWithFallback
 from ufo2ft.filters import loadFilters
 from ufo2ft.filters.decomposeComponents import DecomposeComponentsFilter
 from ufo2ft.util import _GlyphSet
@@ -193,7 +194,9 @@ class TTFInterpolatablePreProcessor(object):
             for ufo, layerName in zip(ufos, layerNames)
         ]
         self._conversionErrors = [
-            (conversionError or DEFAULT_MAX_ERR) * ufo.info.unitsPerEm for ufo in ufos
+            (conversionError or DEFAULT_MAX_ERR)
+            * getAttrWithFallback(ufo.info, "unitsPerEm")
+            for ufo in ufos
         ]
         self._reverseDirection = reverseDirection
         self._rememberCurveType = rememberCurveType
diff --git a/Lib/ufo2ft/util.py b/Lib/ufo2ft/util.py
index d6e4122..853144c 100644
--- a/Lib/ufo2ft/util.py
+++ b/Lib/ufo2ft/util.py
@@ -105,11 +105,9 @@ def _copyLayer(layer, obj_type=dict):
     else:
 
         def newGlyph(name):
-            # use instantiateGlyphObject() to keep any custom sub-element classes
-            # https://github.com/googlefonts/ufo2ft/issues/363
-            g2 = g.layer.instantiateGlyphObject()
-            g2.name = name
-            return g2
+            g = cls()
+            g.name = name
+            return g
 
     # copy everything except unused attributes: 'guidelines', 'note', 'image'
     glyphSet = obj_type()


Test patch:
diff --git a/tests/fontInfoData_test.py b/tests/fontInfoData_test.py
index 20ef9d8..24f913c 100644
--- a/tests/fontInfoData_test.py
+++ b/tests/fontInfoData_test.py
@@ -172,6 +172,25 @@ class GetAttrWithFallbackTest(object):
             del os.environ["SOURCE_DATE_EPOCH"]
         assert getAttrWithFallback(info, "openTypeHeadCreated") != "2017/12/28 18:19:43"
 
+    def test_empty_info(self, InfoClass):
+        info = InfoClass()
+        assert getAttrWithFallback(info, "familyName") == "New Font"
+        assert getAttrWithFallback(info, "styleName") == "Regular"
+        assert getAttrWithFallback(info, "unitsPerEm") == 1000
+        assert getAttrWithFallback(info, "ascender") == 800
+        assert getAttrWithFallback(info, "capHeight") == 700
+        assert getAttrWithFallback(info, "xHeight") == 500
+        assert getAttrWithFallback(info, "descender") == -200
+
+    def test_empty_info_2048(self, InfoClass):
+        info = InfoClass()
+        info.unitsPerEm = 2048
+        assert getAttrWithFallback(info, "unitsPerEm") == 2048
+        assert getAttrWithFallback(info, "ascender") == 1638
+        assert getAttrWithFallback(info, "capHeight") == 1434
+        assert getAttrWithFallback(info, "xHeight") == 1024
+        assert getAttrWithFallback(info, "descender") == -410
+
 
 class PostscriptBlueScaleFallbackTest(object):
     def test_without_blue_zones(self, info):
diff --git a/tests/outlineCompiler_test.py b/tests/outlineCompiler_test.py
index 747cdc1..d67e2e7 100644
--- a/tests/outlineCompiler_test.py
+++ b/tests/outlineCompiler_test.py
@@ -961,6 +961,19 @@ def test_compilation_from_ds_missing_source_font(designspace):
         compileInterpolatableTTFsFromDS(designspace)
 
 
+def test_compile_empty_ufo(FontClass):
+    ufo = FontClass()
+    font = compileTTF(ufo)
+    assert font["name"].getName(1, 3, 1).toUnicode() == "New Font"
+    assert font["name"].getName(2, 3, 1).toUnicode() == "Regular"
+    assert font["name"].getName(4, 3, 1).toUnicode() == "New Font Regular"
+    assert font["head"].unitsPerEm == 1000
+    assert font["OS/2"].sTypoAscender == 800
+    assert font["OS/2"].sCapHeight == 700
+    assert font["OS/2"].sxHeight == 500
+    assert font["OS/2"].sTypoDescender == -200
+
+
 if __name__ == "__main__":
     import sys