From 1122a2612b8daab832f1d56ca9bff4da74d2dc5c Mon Sep 17 00:00:00 2001 From: Cosimo Lupo Date: Thu, 20 Jun 2019 12:22:24 +0100 Subject: [PATCH] varLib: add set_default_weight_width_slant When building a variable font, varLib.build must make sure that the OS/2.usWeightClass is equal to the wght axis default location, that the OS/2.usWidthClass is set to the equivalent value (1-9) of the wdth axis default location, and finally that post.italicAngle is set to the same default value as slnt axis. Sometimes the base master doesn't have these values correctly set leading to discrepancies between OS/2 and post, on the one hand, and the fvar axes' default values. --- Lib/fontTools/varLib/__init__.py | 43 +++++++++++ Tests/varLib/data/test_results/BuildMain.ttx | 2 +- .../data/test_results/SparseMasters.ttx | 2 +- Tests/varLib/varLib_test.py | 77 +++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/Lib/fontTools/varLib/__init__.py b/Lib/fontTools/varLib/__init__.py index 891e92c54..66b193b2f 100644 --- a/Lib/fontTools/varLib/__init__.py +++ b/Lib/fontTools/varLib/__init__.py @@ -857,6 +857,45 @@ def load_designspace(designspace): ) +# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass +WDTH_VALUE_TO_OS2_WIDTH_CLASS = { + 50: 1, + 62.5: 2, + 75: 3, + 87.5: 4, + 100: 5, + 112.5: 6, + 125: 7, + 150: 8, + 200: 9, +} + + +def set_default_weight_width_slant(font, location): + if "OS/2" in font: + if "wght" in location: + weight_class = otRound(max(1, min(location["wght"], 1000))) + if font["OS/2"].usWeightClass != weight_class: + log.info("Setting OS/2.usWidthClass = %s", weight_class) + font["OS/2"].usWeightClass = weight_class + + if "wdth" in location: + # map 'wdth' axis (50..200) to OS/2.usWidthClass (1..9), rounding to closest + widthValue = min(max(location["wdth"], 50), 200) + widthClass = otRound( + models.piecewiseLinearMap(widthValue, WDTH_VALUE_TO_OS2_WIDTH_CLASS) + ) + if font["OS/2"].usWidthClass != widthClass: + log.info("Setting OS/2.usWidthClass = %s", widthClass) + font["OS/2"].usWidthClass = widthClass + + if "slnt" in location and "post" in font: + italicAngle = max(-90, min(location["slnt"], 90)) + if font["post"].italicAngle != italicAngle: + log.info("Setting post.italicAngle = %s", italicAngle) + font["post"].italicAngle = italicAngle + + def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True): """ Build variation font from a designspace file. @@ -930,6 +969,10 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True): post.extraNames = [] post.mapping = {} + set_default_weight_width_slant( + vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes} + ) + for tag in exclude: if tag in vf: del vf[tag] diff --git a/Tests/varLib/data/test_results/BuildMain.ttx b/Tests/varLib/data/test_results/BuildMain.ttx index 7150a5793..7e5d9561f 100644 --- a/Tests/varLib/data/test_results/BuildMain.ttx +++ b/Tests/varLib/data/test_results/BuildMain.ttx @@ -55,7 +55,7 @@ will be recalculated by the compiler --> - + diff --git a/Tests/varLib/data/test_results/SparseMasters.ttx b/Tests/varLib/data/test_results/SparseMasters.ttx index 06e58c92f..c2aa335ce 100644 --- a/Tests/varLib/data/test_results/SparseMasters.ttx +++ b/Tests/varLib/data/test_results/SparseMasters.ttx @@ -55,7 +55,7 @@ will be recalculated by the compiler --> - + diff --git a/Tests/varLib/varLib_test.py b/Tests/varLib/varLib_test.py index e29befbf4..ec05d5657 100644 --- a/Tests/varLib/varLib_test.py +++ b/Tests/varLib/varLib_test.py @@ -4,6 +4,7 @@ from fontTools.ttLib import TTFont, newTable from fontTools.varLib import build from fontTools.varLib.mutator import instantiateVariableFont from fontTools.varLib import main as varLib_main, load_masters +from fontTools.varLib import set_default_weight_width_slant from fontTools.designspaceLib import ( DesignSpaceDocumentError, DesignSpaceDocument, SourceDescriptor, ) @@ -661,5 +662,81 @@ def _extract_flat_kerning(font, pairpos_table): return extracted_kerning +@pytest.fixture +def ttFont(): + f = TTFont() + f["OS/2"] = newTable("OS/2") + f["OS/2"].usWeightClass = 400 + f["OS/2"].usWidthClass = 100 + f["post"] = newTable("post") + f["post"].italicAngle = 0 + return f + + +class SetDefaultWeightWidthSlantTest(object): + @pytest.mark.parametrize( + "location, expected", + [ + ({"wght": 0}, 1), + ({"wght": 1}, 1), + ({"wght": 100}, 100), + ({"wght": 1000}, 1000), + ({"wght": 1001}, 1000), + ], + ) + def test_wght(self, ttFont, location, expected): + set_default_weight_width_slant(ttFont, location) + + assert ttFont["OS/2"].usWeightClass == expected + + @pytest.mark.parametrize( + "location, expected", + [ + ({"wdth": 0}, 1), + ({"wdth": 56}, 1), + ({"wdth": 57}, 2), + ({"wdth": 62.5}, 2), + ({"wdth": 75}, 3), + ({"wdth": 87.5}, 4), + ({"wdth": 100}, 5), + ({"wdth": 112.5}, 6), + ({"wdth": 125}, 7), + ({"wdth": 150}, 8), + ({"wdth": 200}, 9), + ({"wdth": 201}, 9), + ({"wdth": 1000}, 9), + ], + ) + def test_wdth(self, ttFont, location, expected): + set_default_weight_width_slant(ttFont, location) + + assert ttFont["OS/2"].usWidthClass == expected + + @pytest.mark.parametrize( + "location, expected", + [ + ({"slnt": -91}, -90), + ({"slnt": -90}, -90), + ({"slnt": 0}, 0), + ({"slnt": 11.5}, 11.5), + ({"slnt": 90}, 90), + ({"slnt": 91}, 90), + ], + ) + def test_slnt(self, ttFont, location, expected): + set_default_weight_width_slant(ttFont, location) + + assert ttFont["post"].italicAngle == expected + + def test_all(self, ttFont): + set_default_weight_width_slant( + ttFont, {"wght": 500, "wdth": 150, "slnt": -12.0} + ) + + assert ttFont["OS/2"].usWeightClass == 500 + assert ttFont["OS/2"].usWidthClass == 8 + assert ttFont["post"].italicAngle == -12.0 + + if __name__ == "__main__": sys.exit(unittest.main())